0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

feat(console): implement mfa config page (#4492)

* feat(console): implement mfa config page

* test(console): add ui tests for mfa configuration

* chore(console): remove unused eslint command comment
This commit is contained in:
Xiao Yijun 2023-09-13 15:14:59 +08:00 committed by GitHub
parent acae0b784f
commit fcacbbbcc8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 1127 additions and 11 deletions

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 16.6666H4.16666C3.94565 16.6666 3.73369 16.5788 3.57741 16.4225C3.42113 16.2663 3.33333 16.0543 3.33333 15.8333V4.16663C3.33333 3.94561 3.42113 3.73365 3.57741 3.57737C3.73369 3.42109 3.94565 3.33329 4.16666 3.33329H8.33333V5.83329C8.33333 6.49633 8.59672 7.13222 9.06556 7.60106C9.5344 8.0699 10.1703 8.33329 10.8333 8.33329H13.3333V9.16663C13.3333 9.38764 13.4211 9.5996 13.5774 9.75588C13.7337 9.91216 13.9457 9.99996 14.1667 9.99996C14.3877 9.99996 14.5996 9.91216 14.7559 9.75588C14.9122 9.5996 15 9.38764 15 9.16663V7.49996C15 7.49996 15 7.49996 15 7.44996C14.9913 7.3734 14.9746 7.29799 14.95 7.22496V7.14996C14.9099 7.06428 14.8565 6.98551 14.7917 6.91663L9.79166 1.91663C9.72278 1.85181 9.64401 1.79836 9.55833 1.75829C9.53346 1.75476 9.50821 1.75476 9.48333 1.75829C9.39868 1.70974 9.30518 1.67858 9.20833 1.66663H4.16666C3.50362 1.66663 2.86774 1.93002 2.3989 2.39886C1.93006 2.8677 1.66666 3.50358 1.66666 4.16663V15.8333C1.66666 16.4963 1.93006 17.1322 2.3989 17.6011C2.86774 18.0699 3.50362 18.3333 4.16666 18.3333H10C10.221 18.3333 10.433 18.2455 10.5893 18.0892C10.7455 17.9329 10.8333 17.721 10.8333 17.5C10.8333 17.2789 10.7455 17.067 10.5893 16.9107C10.433 16.7544 10.221 16.6666 10 16.6666ZM10 4.50829L12.1583 6.66663H10.8333C10.6123 6.66663 10.4004 6.57883 10.2441 6.42255C10.0878 6.26627 10 6.05431 10 5.83329V4.50829ZM5.83333 6.66663C5.61232 6.66663 5.40036 6.75442 5.24408 6.9107C5.0878 7.06698 5 7.27895 5 7.49996C5 7.72097 5.0878 7.93293 5.24408 8.08922C5.40036 8.2455 5.61232 8.33329 5.83333 8.33329H6.66666C6.88768 8.33329 7.09964 8.2455 7.25592 8.08922C7.4122 7.93293 7.5 7.72097 7.5 7.49996C7.5 7.27895 7.4122 7.06698 7.25592 6.9107C7.09964 6.75442 6.88768 6.66663 6.66666 6.66663H5.83333ZM18.0917 16.9083L17.1167 15.9416C17.4283 15.3977 17.553 14.7666 17.4718 14.1449C17.3905 13.5233 17.1077 12.9455 16.6667 12.5C16.2609 12.0797 15.7379 11.7914 15.1659 11.6726C14.5938 11.5538 13.9993 11.61 13.4597 11.8339C12.9201 12.0579 12.4605 12.4391 12.1406 12.928C11.8208 13.4169 11.6557 13.9908 11.6667 14.575C11.6638 15.0776 11.7924 15.5723 12.0397 16.0099C12.287 16.4475 12.6444 16.8129 13.0764 17.0698C13.5085 17.3267 14.0002 17.4661 14.5028 17.4744C15.0054 17.4826 15.5014 17.3593 15.9417 17.1166L16.9083 18.0916C16.9858 18.1697 17.078 18.2317 17.1795 18.274C17.2811 18.3163 17.39 18.3381 17.5 18.3381C17.61 18.3381 17.7189 18.3163 17.8205 18.274C17.922 18.2317 18.0142 18.1697 18.0917 18.0916C18.1698 18.0142 18.2318 17.922 18.2741 17.8204C18.3164 17.7189 18.3382 17.61 18.3382 17.5C18.3382 17.3899 18.3164 17.281 18.2741 17.1795C18.2318 17.0779 18.1698 16.9858 18.0917 16.9083ZM15.45 15.45C15.212 15.6738 14.8976 15.7984 14.5708 15.7984C14.2441 15.7984 13.9297 15.6738 13.6917 15.45C13.4623 15.2165 13.3336 14.9023 13.3333 14.575C13.3316 14.4106 13.3632 14.2476 13.4262 14.0958C13.4892 13.9439 13.5824 13.8065 13.7 13.6916C13.9222 13.4707 14.2201 13.3425 14.5333 13.3333C14.7018 13.3229 14.8706 13.3475 15.0291 13.4055C15.1877 13.4634 15.3325 13.5535 15.4546 13.6701C15.5766 13.7867 15.6733 13.9273 15.7385 14.083C15.8037 14.2387 15.8359 14.4062 15.8333 14.575C15.8264 14.9059 15.6886 15.2205 15.45 15.45ZM10.8333 9.99996H5.83333C5.61232 9.99996 5.40036 10.0878 5.24408 10.244C5.0878 10.4003 5 10.6123 5 10.8333C5 11.0543 5.0878 11.2663 5.24408 11.4225C5.40036 11.5788 5.61232 11.6666 5.83333 11.6666H10.8333C11.0543 11.6666 11.2663 11.5788 11.4226 11.4225C11.5789 11.2663 11.6667 11.0543 11.6667 10.8333C11.6667 10.6123 11.5789 10.4003 11.4226 10.244C11.2663 10.0878 11.0543 9.99996 10.8333 9.99996ZM9.16666 15C9.38768 15 9.59964 14.9122 9.75592 14.7559C9.9122 14.5996 10 14.3876 10 14.1666C10 13.9456 9.9122 13.7337 9.75592 13.5774C9.59964 13.4211 9.38768 13.3333 9.16666 13.3333H5.83333C5.61232 13.3333 5.40036 13.4211 5.24408 13.5774C5.0878 13.7337 5 13.9456 5 14.1666C5 14.3876 5.0878 14.5996 5.24408 14.7559C5.40036 14.9122 5.61232 15 5.83333 15H9.16666Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.5001 1.66663H12.5001C12.2791 1.66663 12.0671 1.75442 11.9108 1.9107C11.7545 2.06698 11.6667 2.27895 11.6667 2.49996C11.6667 2.72097 11.7545 2.93293 11.9108 3.08922C12.0671 3.2455 12.2791 3.33329 12.5001 3.33329H16.6667V7.49996C16.6667 7.72097 16.7545 7.93293 16.9108 8.08922C17.0671 8.2455 17.2791 8.33329 17.5001 8.33329C17.7211 8.33329 17.9331 8.2455 18.0893 8.08922C18.2456 7.93293 18.3334 7.72097 18.3334 7.49996V2.49996C18.3334 2.27895 18.2456 2.06698 18.0893 1.9107C17.9331 1.75442 17.7211 1.66663 17.5001 1.66663ZM17.5001 11.6666C17.2791 11.6666 17.0671 11.7544 16.9108 11.9107C16.7545 12.067 16.6667 12.2789 16.6667 12.5V16.6666H12.5001C12.2791 16.6666 12.0671 16.7544 11.9108 16.9107C11.7545 17.067 11.6667 17.2789 11.6667 17.5C11.6667 17.721 11.7545 17.9329 11.9108 18.0892C12.0671 18.2455 12.2791 18.3333 12.5001 18.3333H17.5001C17.7211 18.3333 17.9331 18.2455 18.0893 18.0892C18.2456 17.9329 18.3334 17.721 18.3334 17.5V12.5C18.3334 12.2789 18.2456 12.067 18.0893 11.9107C17.9331 11.7544 17.7211 11.6666 17.5001 11.6666ZM10.0001 4.99996C9.33704 4.99996 8.70116 5.26335 8.23231 5.73219C7.76347 6.20103 7.50008 6.83692 7.50008 7.49996V8.33329C7.05805 8.33329 6.63413 8.50889 6.32157 8.82145C6.00901 9.13401 5.83342 9.55793 5.83342 9.99996V13.3333C5.83342 13.7753 6.00901 14.1992 6.32157 14.5118C6.63413 14.8244 7.05805 15 7.50008 15H12.5001C12.9421 15 13.366 14.8244 13.6786 14.5118C13.9912 14.1992 14.1667 13.7753 14.1667 13.3333V9.99996C14.1667 9.55793 13.9912 9.13401 13.6786 8.82145C13.366 8.50889 12.9421 8.33329 12.5001 8.33329V7.49996C12.5001 6.83692 12.2367 6.20103 11.7678 5.73219C11.299 5.26335 10.6631 4.99996 10.0001 4.99996ZM9.16675 7.49996C9.16675 7.27895 9.25455 7.06698 9.41083 6.9107C9.56711 6.75442 9.77907 6.66663 10.0001 6.66663C10.2211 6.66663 10.4331 6.75442 10.5893 6.9107C10.7456 7.06698 10.8334 7.27895 10.8334 7.49996V8.33329H9.16675V7.49996ZM12.5001 13.3333H7.50008V9.99996H12.5001V13.3333ZM2.50008 8.33329C2.7211 8.33329 2.93306 8.2455 3.08934 8.08922C3.24562 7.93293 3.33341 7.72097 3.33341 7.49996V3.33329H7.50008C7.7211 3.33329 7.93306 3.2455 8.08934 3.08922C8.24562 2.93293 8.33342 2.72097 8.33342 2.49996C8.33342 2.27895 8.24562 2.06698 8.08934 1.9107C7.93306 1.75442 7.7211 1.66663 7.50008 1.66663H2.50008C2.27907 1.66663 2.06711 1.75442 1.91083 1.9107C1.75455 2.06698 1.66675 2.27895 1.66675 2.49996V7.49996C1.66675 7.72097 1.75455 7.93293 1.91083 8.08922C2.06711 8.2455 2.27907 8.33329 2.50008 8.33329ZM7.50008 16.6666H3.33341V12.5C3.33341 12.2789 3.24562 12.067 3.08934 11.9107C2.93306 11.7544 2.7211 11.6666 2.50008 11.6666C2.27907 11.6666 2.06711 11.7544 1.91083 11.9107C1.75455 12.067 1.66675 12.2789 1.66675 12.5V17.5C1.66675 17.721 1.75455 17.9329 1.91083 18.0892C2.06711 18.2455 2.27907 18.3333 2.50008 18.3333H7.50008C7.7211 18.3333 7.93306 18.2455 8.08934 18.0892C8.24562 17.9329 8.33342 17.721 8.33342 17.5C8.33342 17.2789 8.24562 17.067 8.08934 16.9107C7.93306 16.7544 7.7211 16.6666 7.50008 16.6666Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -0,0 +1,19 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1158_5594)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 2.91659C10 2.68647 9.81345 2.49992 9.58333 2.49992H4.58333C4.35321 2.49992 4.16667 2.68647 4.16667 2.91659V5.83325H10V2.91659ZM4.16667 0.833252C3.24619 0.833252 2.5 1.57944 2.5 2.49992V5.83325C2.5 6.75373 3.24619 6.66658 4.16667 6.66658H10C10.9205 6.66658 11.6667 6.75373 11.6667 5.83325V2.49992C11.6667 1.57944 10.9205 0.833252 10 0.833252H4.16667Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.833336 7.49992C0.833336 6.57944 1.57953 5.83325 2.5 5.83325H11.6667C12.5871 5.83325 13.3333 6.57944 13.3333 7.49992L2.91667 7.49992C2.68655 7.49992 2.5 7.68647 2.5 7.91659V17.0833C2.5 17.3134 2.68655 17.4999 2.91667 17.4999H5C5.46024 17.4999 5.83334 17.873 5.83334 18.3333C5.83334 18.7935 5.46024 19.1666 5 19.1666H2.5C1.57953 19.1666 0.833336 18.4204 0.833336 17.4999V7.49992Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.83334 4.99992C5.3731 4.99992 5.00001 4.62682 5.00001 4.16659C5.00001 3.70635 5.3731 3.33325 5.83334 3.33325C6.29358 3.33325 6.66667 3.70635 6.66667 4.16659C6.66667 4.62682 6.29358 4.99992 5.83334 4.99992Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.33334 4.99992C7.8731 4.99992 7.5 4.62682 7.5 4.16659C7.5 3.70635 7.8731 3.33325 8.33334 3.33325C8.79358 3.33325 9.16667 3.70635 9.16667 4.16659C9.16667 4.62682 8.79358 4.99992 8.33334 4.99992Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.75 9.99992C11.6024 9.99992 10 11.5653 10 13.3333V18.3333C10 18.7935 9.62691 19.1666 9.16667 19.1666C8.70643 19.1666 8.33334 18.7935 8.33334 18.3333V13.3333C8.33334 10.4989 10.835 8.33325 13.75 8.33325C16.665 8.33325 19.1667 10.4989 19.1667 13.3333V14.9999C19.1667 15.4602 18.7936 15.8333 18.3333 15.8333C17.8731 15.8333 17.5 15.4602 17.5 14.9999V13.3333C17.5 11.5653 15.8976 9.99992 13.75 9.99992Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.75 12.4999C14.5169 12.4999 15 13.0477 15 13.5605V18.3333C15 18.7935 15.3731 19.1666 15.8333 19.1666C16.2936 19.1666 16.6667 18.7935 16.6667 18.3333V13.5605C16.6667 11.9813 15.2843 10.8333 13.75 10.8333C12.2157 10.8333 10.8333 11.9813 10.8333 13.5605V14.3181C10.8333 14.7783 11.2064 15.1514 11.6667 15.1514C12.1269 15.1514 12.5 14.7783 12.5 14.3181V13.5605C12.5 13.0477 12.9831 12.4999 13.75 12.4999Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.3333 16.6665C18.7936 16.6665 19.1667 17.0396 19.1667 17.4998L19.1667 18.3332C19.1667 18.7934 18.7936 19.1665 18.3333 19.1665C17.8731 19.1665 17.5 18.7934 17.5 18.3332L17.5 17.4998C17.5 17.0396 17.8731 16.6665 18.3333 16.6665Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.75 16.6665C14.2102 16.6665 14.5833 17.0396 14.5833 17.4998L14.5833 18.3332C14.5833 18.7934 14.2102 19.1665 13.75 19.1665C13.2898 19.1665 12.9167 18.7934 12.9167 18.3332L12.9167 17.4998C12.9167 17.0396 13.2898 16.6665 13.75 16.6665Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.6667 15.8333C12.1269 15.8333 12.5 16.2063 12.5 16.6666L12.5 18.3333C12.5 18.7935 12.1269 19.1666 11.6667 19.1666C11.2064 19.1666 10.8333 18.7935 10.8333 18.3333L10.8333 16.6666C10.8333 16.2063 11.2064 15.8333 11.6667 15.8333Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.75 13.3333C14.2102 13.3333 14.5833 13.7063 14.5833 14.1666V14.9999C14.5833 15.4602 14.2102 15.8333 13.75 15.8333C13.2898 15.8333 12.9167 15.4602 12.9167 14.9999V14.1666C12.9167 13.7063 13.2898 13.3333 13.75 13.3333Z" fill="currentColor"/>
</g>
<defs>
<clipPath id="clip0_1158_5594">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.99998 10.8333C9.74627 10.8303 9.49774 10.9052 9.28785 11.0477C9.07795 11.1903 8.91677 11.3937 8.82599 11.6306C8.73521 11.8676 8.7192 12.1266 8.7801 12.3729C8.84101 12.6192 8.97591 12.841 9.16665 13.0083V14.1666C9.16665 14.3876 9.25444 14.5996 9.41072 14.7559C9.567 14.9122 9.77897 15 9.99998 15C10.221 15 10.433 14.9122 10.5892 14.7559C10.7455 14.5996 10.8333 14.3876 10.8333 14.1666V13.0083C11.0241 12.841 11.159 12.6192 11.2199 12.3729C11.2808 12.1266 11.2647 11.8676 11.174 11.6306C11.0832 11.3937 10.922 11.1903 10.7121 11.0477C10.5022 10.9052 10.2537 10.8303 9.99998 10.8333ZM14.1666 7.49996V5.83329C14.1666 4.72822 13.7277 3.66842 12.9463 2.88701C12.1649 2.10561 11.105 1.66663 9.99998 1.66663C8.89491 1.66663 7.8351 2.10561 7.0537 2.88701C6.2723 3.66842 5.83331 4.72822 5.83331 5.83329V7.49996C5.17027 7.49996 4.53439 7.76335 4.06555 8.23219C3.59671 8.70103 3.33331 9.33692 3.33331 9.99996V15.8333C3.33331 16.4963 3.59671 17.1322 4.06555 17.6011C4.53439 18.0699 5.17027 18.3333 5.83331 18.3333H14.1666C14.8297 18.3333 15.4656 18.0699 15.9344 17.6011C16.4033 17.1322 16.6666 16.4963 16.6666 15.8333V9.99996C16.6666 9.33692 16.4033 8.70103 15.9344 8.23219C15.4656 7.76335 14.8297 7.49996 14.1666 7.49996ZM7.49998 5.83329C7.49998 5.17025 7.76337 4.53437 8.23221 4.06553C8.70105 3.59668 9.33694 3.33329 9.99998 3.33329C10.663 3.33329 11.2989 3.59668 11.7677 4.06553C12.2366 4.53437 12.5 5.17025 12.5 5.83329V7.49996H7.49998V5.83329ZM15 15.8333C15 16.0543 14.9122 16.2663 14.7559 16.4225C14.5996 16.5788 14.3877 16.6666 14.1666 16.6666H5.83331C5.6123 16.6666 5.40034 16.5788 5.24406 16.4225C5.08778 16.2663 4.99998 16.0543 4.99998 15.8333V9.99996C4.99998 9.77895 5.08778 9.56698 5.24406 9.4107C5.40034 9.25442 5.6123 9.16663 5.83331 9.16663H14.1666C14.3877 9.16663 14.5996 9.25442 14.7559 9.4107C14.9122 9.56698 15 9.77895 15 9.99996V15.8333Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -1,6 +1,5 @@
import { yes } from '@silverhand/essentials';
// eslint-disable-next-line import/no-unused-modules
export const isProduction = process.env.NODE_ENV === 'production';
export const isCloud = yes(process.env.IS_CLOUD);
export const adminEndpoint = process.env.ADMIN_ENDPOINT;

View file

@ -12,6 +12,7 @@ import List from '@/assets/icons/list.svg';
import UserProfile from '@/assets/icons/profile.svg';
import ResourceIcon from '@/assets/icons/resource.svg';
import Role from '@/assets/icons/role.svg';
import SecurityLock from '@/assets/icons/security-lock.svg';
import Web from '@/assets/icons/web.svg';
import { isCloud } from '@/consts/env';
import useUserPreferences from '@/hooks/use-user-preferences';
@ -78,6 +79,10 @@ export const useSidebarMenuItems = (): {
Icon: Web,
title: 'sign_in_experience',
},
{
Icon: SecurityLock,
title: 'mfa',
},
{
Icon: Connection,
title: 'connectors',

View file

@ -9,7 +9,7 @@ import {
TenantSettingsTabs,
ApplicationDetailsTabs,
} from '@/consts';
import { isCloud } from '@/consts/env';
import { isCloud, isProduction } from '@/consts/env';
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
import useUserPreferences from '@/hooks/use-user-preferences';
import ApiResourceDetails from '@/pages/ApiResourceDetails';
@ -24,6 +24,7 @@ import ConnectorDetails from '@/pages/ConnectorDetails';
import Connectors from '@/pages/Connectors';
import Dashboard from '@/pages/Dashboard';
import GetStarted from '@/pages/GetStarted';
import Mfa from '@/pages/Mfa';
import NotFound from '@/pages/NotFound';
import Profile from '@/pages/Profile';
import ChangePasswordModal from '@/pages/Profile/containers/ChangePasswordModal';
@ -97,6 +98,7 @@ function ConsoleContent() {
<Route index element={<Navigate replace to={SignInExperienceTab.Branding} />} />
<Route path=":tab" element={<SignInExperience />} />
</Route>
{!isProduction && <Route path="mfa" element={<Mfa />} />}
<Route path="connectors">
<Route index element={<Navigate replace to={ConnectorsTabs.Passwordless} />} />
<Route path=":tab" element={<Connectors />} />

View file

@ -58,4 +58,12 @@
margin-right: _.unit(2);
font: var(--font-body-2);
}
&.error {
border-color: var(--color-error);
&:focus-within {
outline-color: var(--color-danger-focused);
}
}
}

View file

@ -1,3 +1,4 @@
import classNames from 'classnames';
import type { HTMLProps, ReactNode, Ref } from 'react';
import { forwardRef } from 'react';
@ -5,11 +6,12 @@ import * as styles from './index.module.scss';
type Props = Omit<HTMLProps<HTMLInputElement>, 'label'> & {
label?: ReactNode;
hasError?: boolean;
};
function Switch({ label, ...rest }: Props, ref?: Ref<HTMLInputElement>) {
function Switch({ label, hasError, ...rest }: Props, ref?: Ref<HTMLInputElement>) {
return (
<div className={styles.wrapper}>
<div className={classNames(styles.wrapper, hasError && styles.error)}>
<div className={styles.label}>{label}</div>
<label className={styles.switch}>
<input type="checkbox" {...rest} ref={ref} />

View file

@ -0,0 +1,20 @@
@use '@/scss/underscore' as _;
.factorLabel {
display: flex;
flex-direction: column;
gap: _.unit(1);
font: var(--font-body-2);
color: var(--color-text-secondary);
}
.factorTitle {
display: flex;
align-items: center;
color: var(--color-text);
.factorIcon {
color: var(--color-text-secondary);
margin-right: _.unit(3);
}
}

View file

@ -0,0 +1,28 @@
import { type AdminConsoleKey } from '@logto/phrases';
import { type ReactElement, cloneElement } from 'react';
import DynamicT from '@/ds-components/DynamicT';
import * as styles from './index.module.scss';
type Props = {
title: AdminConsoleKey;
description: AdminConsoleKey;
icon: ReactElement;
};
function FactorLabel({ title, description, icon }: Props) {
return (
<div className={styles.factorLabel}>
<div className={styles.factorTitle}>
{cloneElement(icon, { className: styles.factorIcon })}
<DynamicT forKey={title} />
</div>
<div>
<DynamicT forKey={description} />
</div>
</div>
);
}
export default FactorLabel;

View file

@ -0,0 +1,12 @@
@use '@/scss/underscore' as _;
.policyLabel {
font: var(--font-body-2);
display: flex;
flex-direction: column;
gap: _.unit(1);
}
.description {
color: var(--color-text-secondary);
}

View file

@ -0,0 +1,25 @@
import { type AdminConsoleKey } from '@logto/phrases';
import DynamicT from '@/ds-components/DynamicT';
import * as styles from './index.module.scss';
export type Props = {
title: AdminConsoleKey;
description: AdminConsoleKey;
};
function PolicyOptionTitle({ title, description }: Props) {
return (
<div className={styles.policyLabel}>
<div>
<DynamicT forKey={title} />
</div>
<div className={styles.description}>
<DynamicT forKey={description} />
</div>
</div>
);
}
export default PolicyOptionTitle;

View file

@ -0,0 +1,14 @@
import { MfaPolicy } from '@logto/schemas';
import { type Props as PolicyOptionTitleProps } from './PolicyOptionTitle';
export const policyOptionTitlePropsMap: Record<MfaPolicy, PolicyOptionTitleProps> = {
[MfaPolicy.UserControlled]: {
title: 'mfa.user_controlled',
description: 'mfa.user_controlled_description',
},
[MfaPolicy.Mandatory]: {
title: 'mfa.mandatory',
description: 'mfa.mandatory_description',
},
};

View file

@ -0,0 +1,31 @@
@use '@/scss/underscore' as _;
.factorField {
display: flex;
flex-direction: column;
gap: _.unit(4);
}
.fieldDescription {
font: var(--font-body-2);
color: var(--color-text-secondary);
margin-bottom: _.unit(2);
}
.backupCodeField {
display: flex;
flex-direction: column;
gap: _.unit(2);
.backupCodeDescription {
margin-bottom: unset;
}
}
.policyRadio {
> div[class$='content'] {
> div[class$='indicator'] {
align-self: flex-start;
}
}
}

View file

@ -0,0 +1,162 @@
import { MfaPolicy, type SignInExperience } from '@logto/schemas';
import classNames from 'classnames';
import { useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import FactorBackupCode from '@/assets/icons/factor-backup-code.svg';
import FactorOtp from '@/assets/icons/factor-totp.svg';
import FactorWebAuthn from '@/assets/icons/factor-webauthn.svg';
import DetailsForm from '@/components/DetailsForm';
import FormCard from '@/components/FormCard';
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
import DynamicT from '@/ds-components/DynamicT';
import FormField from '@/ds-components/FormField';
import InlineNotification from '@/ds-components/InlineNotification';
import RadioGroup, { Radio } from '@/ds-components/RadioGroup';
import Switch from '@/ds-components/Switch';
import useApi from '@/hooks/use-api';
import { trySubmitSafe } from '@/utils/form';
import { type MfaConfigForm, type MfaConfig } from '../types';
import FactorLabel from './FactorLabel';
import PolicyOptionTitle from './PolicyOptionTitle';
import { policyOptionTitlePropsMap } from './constants';
import * as styles from './index.module.scss';
import { convertMfaFormToConfig, convertMfaConfigToForm, validateBackupCodeFactor } from './utils';
type Props = {
data: MfaConfig;
onMfaUpdated: (updatedData: MfaConfig) => void;
};
function MfaForm({ data, onMfaUpdated }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const {
register,
reset,
formState: { isDirty, isSubmitting },
handleSubmit,
control,
watch,
} = useForm<MfaConfigForm>({ defaultValues: convertMfaConfigToForm(data), mode: 'onChange' });
const api = useApi();
const formValues = watch();
const isBackupCodeAllowed = useMemo(() => {
const { factors } = convertMfaFormToConfig(formValues);
return validateBackupCodeFactor(factors);
}, [formValues]);
const onSubmit = handleSubmit(
trySubmitSafe(async (formData) => {
const mfaConfig = convertMfaFormToConfig(formData);
if (!validateBackupCodeFactor(mfaConfig.factors)) {
return;
}
const { mfa: updatedMfaConfig } = await api
.patch('api/sign-in-exp', {
json: { mfa: mfaConfig },
})
.json<SignInExperience>();
reset(convertMfaConfigToForm(updatedMfaConfig));
toast.success(t('general.saved'));
onMfaUpdated(updatedMfaConfig);
})
);
return (
<>
<DetailsForm
isDirty={isDirty}
isSubmitting={isSubmitting}
onSubmit={onSubmit}
onDiscard={reset}
>
<FormCard title="mfa.factors">
<FormField title="mfa.multi_factors">
<div className={styles.fieldDescription}>
<DynamicT forKey="mfa.multi_factors_description" />
</div>
<div className={styles.factorField}>
<Switch
label={
<FactorLabel
title="mfa.totp"
description="mfa.otp_description"
icon={<FactorOtp />}
/>
}
{...register('totpEnabled')}
/>
<Switch
label={
<FactorLabel
title="mfa.webauthn"
description="mfa.webauthn_description"
icon={<FactorWebAuthn />}
/>
}
{...register('webAuthnEnabled')}
/>
<div className={styles.backupCodeField}>
<div className={classNames(styles.fieldDescription, styles.backupCodeDescription)}>
<DynamicT forKey="mfa.backup_code_setup_hint" />
</div>
<Switch
label={
<FactorLabel
title="mfa.backup_code"
description="mfa.backup_code_description"
icon={<FactorBackupCode />}
/>
}
hasError={!isBackupCodeAllowed}
{...register('backupCodeEnabled')}
/>
{!isBackupCodeAllowed && (
<InlineNotification>
<DynamicT forKey="mfa.backup_code_error_hint" />
</InlineNotification>
)}
</div>
</div>
</FormField>
</FormCard>
<FormCard title="mfa.policy">
<FormField title="mfa.two_step_sign_in_policy">
<div className={styles.fieldDescription}>
<DynamicT forKey="mfa.two_step_sign_in_policy_description" />
</div>
<Controller
control={control}
name="policy"
render={({ field: { onChange, value, name } }) => (
<RadioGroup name={name} value={value} onChange={onChange}>
{Object.values(MfaPolicy).map((policy) => {
const titleProps = policyOptionTitlePropsMap[policy];
return (
<Radio
key={policy}
className={styles.policyRadio}
title={<PolicyOptionTitle {...titleProps} />}
value={policy}
/>
);
})}
</RadioGroup>
)}
/>
</FormField>
</FormCard>
</DetailsForm>
<UnsavedChangesAlertModal hasUnsavedChanges={isDirty} />
</>
);
}
export default MfaForm;

View file

@ -0,0 +1,31 @@
import { MfaFactor } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { type MfaConfig, type MfaConfigForm } from '../types';
export const convertMfaConfigToForm = ({ policy, factors }: MfaConfig): MfaConfigForm => ({
policy,
totpEnabled: factors.includes(MfaFactor.TOTP),
webAuthnEnabled: factors.includes(MfaFactor.WebAuthn),
backupCodeEnabled: factors.includes(MfaFactor.BackupCode),
});
export const convertMfaFormToConfig = (mfaConfigForm: MfaConfigForm): MfaConfig => {
const { policy, totpEnabled, webAuthnEnabled, backupCodeEnabled } = mfaConfigForm;
const factors = [
conditional(totpEnabled && MfaFactor.TOTP),
conditional(webAuthnEnabled && MfaFactor.WebAuthn),
conditional(backupCodeEnabled && MfaFactor.BackupCode),
// eslint-disable-next-line unicorn/prefer-native-coercion-functions
].filter((factor): factor is MfaFactor => Boolean(factor));
return {
policy,
factors,
};
};
export const validateBackupCodeFactor = (factors: MfaFactor[]): boolean => {
return !(factors.length === 1 && factors.includes(MfaFactor.BackupCode));
};

View file

@ -0,0 +1,12 @@
@use '@/scss/underscore' as _;
.container {
display: flex;
flex-direction: column;
min-height: 100%;
.cardTitle {
flex-shrink: 0;
margin-bottom: _.unit(4);
}
}

View file

@ -0,0 +1,22 @@
import { type ReactNode } from 'react';
import PageMeta from '@/components/PageMeta';
import CardTitle from '@/ds-components/CardTitle';
import * as styles from './index.module.scss';
type Props = {
children: ReactNode;
};
function PageWrapper({ children }: Props) {
return (
<div className={styles.container}>
<PageMeta titleKey="mfa.title" />
<CardTitle title="mfa.title" subtitle="mfa.description" className={styles.cardTitle} />
{children}
</div>
);
}
export default PageWrapper;

View file

@ -0,0 +1,7 @@
@use '@/scss/underscore' as _;
.container {
display: flex;
flex-direction: column;
gap: _.unit(4);
}

View file

@ -0,0 +1,14 @@
import { FormCardSkeleton } from '@/components/FormCard';
import * as styles from './index.module.scss';
function Skeleton() {
return (
<div className={styles.container}>
<FormCardSkeleton formFieldCount={1} />
<FormCardSkeleton formFieldCount={1} />
</div>
);
}
export default Skeleton;

View file

@ -0,0 +1,47 @@
import { withAppInsights } from '@logto/app-insights/react/AppInsightsReact';
import { type SignInExperience } from '@logto/schemas';
import useSWR from 'swr';
import RequestDataError from '@/components/RequestDataError';
import { type RequestError } from '@/hooks/use-api';
import MfaForm from './MfaForm';
import PageWrapper from './PageWrapper';
import Skeleton from './Skeleton';
function Mfa() {
const { data, error, mutate, isLoading } = useSWR<SignInExperience, RequestError>(
'api/sign-in-exp'
);
if (isLoading) {
return (
<PageWrapper>
<Skeleton />
</PageWrapper>
);
}
if (error) {
return (
<PageWrapper>
<RequestDataError error={error} onRetry={mutate} />
</PageWrapper>
);
}
return (
<PageWrapper>
{data && (
<MfaForm
data={data.mfa}
onMfaUpdated={(mfa) => {
void mutate({ ...data, mfa });
}}
/>
)}
</PageWrapper>
);
}
export default withAppInsights(Mfa);

View file

@ -0,0 +1,10 @@
import { type MfaPolicy, type SignInExperience } from '@logto/schemas';
export type MfaConfig = SignInExperience['mfa'];
export type MfaConfigForm = {
policy: MfaPolicy;
totpEnabled: boolean;
webAuthnEnabled: boolean;
backupCodeEnabled: boolean;
};

View file

@ -0,0 +1,15 @@
import { type Page } from 'puppeteer';
export const expectToClickFactor = async (page: Page, inputName: string) => {
await expect(page).toClick(`form label[class$=switch]:has(input[name="${inputName}"])`);
};
export const expectToClickPolicyOption = async (page: Page, inputName: string) => {
await expect(page).toClick(`form div[role=radio]:has(input[name=policy][value=${inputName}])`);
};
export const expectBackupCodeSetupError = async (page: Page) => {
await expect(page).toMatchElement('form div[class*=inlineNotification] div[class$=content]', {
text: 'To use Backup code for MFA, other factors must be turned on to ensure your users successful sign-in.',
});
};

View file

@ -0,0 +1,63 @@
import { logtoConsoleUrl } from '#src/constants.js';
import {
expectMainPageWithTitle,
expectToClickSidebarMenu,
expectToSaveChanges,
goToAdminConsole,
waitForToast,
} from '#src/ui-helpers/index.js';
import {
expectBackupCodeSetupError,
expectToClickFactor,
expectToClickPolicyOption,
} from './helpers.js';
await page.setViewport({ width: 1920, height: 1080 });
describe('multi-factor authentication', () => {
beforeAll(async () => {
await goToAdminConsole();
});
it('navigate to multi-factor authentication page', async () => {
await expectToClickSidebarMenu(page, 'Multi-factor auth');
await expectMainPageWithTitle(page, 'Multi-factor authentication');
expect(page.url()).toBe(new URL(`console/mfa`, new URL(logtoConsoleUrl)).href);
});
it('should be able to update multi-factors', async () => {
// Cannot enable backup code alone
await expectToClickFactor(page, 'backupCodeEnabled');
await expectBackupCodeSetupError(page);
// Enable webAuthn
await expectToClickFactor(page, 'webAuthnEnabled');
// The backup code error should disappear
await expect(expectBackupCodeSetupError(page)).rejects.toThrow();
await expectToSaveChanges(page);
await waitForToast(page, { text: 'Saved' });
// Enable totp
await expectToClickFactor(page, 'totpEnabled');
await expectToSaveChanges(page);
await waitForToast(page, { text: 'Saved' });
});
it('should be able to update policy', async () => {
await expect(page).toClick('form div[role=radio]:has(input[name=policy][value=Mandatory])');
await expectToClickPolicyOption(page, 'Mandatory');
await expectToSaveChanges(page);
await waitForToast(page, { text: 'Saved' });
});
it('reset mfa settings', async () => {
await expectToClickFactor(page, 'backupCodeEnabled');
await expectToClickFactor(page, 'webAuthnEnabled');
await expectToClickFactor(page, 'totpEnabled');
await expectToClickPolicyOption(page, 'UserControlled');
await expectToSaveChanges(page);
await waitForToast(page, { text: 'Saved' });
});
});

View file

@ -131,11 +131,14 @@ export const expectToOpenNewPage = async (browser: Browser, url: string) => {
};
export const expectMainPageWithTitle = async (page: Page, title: string) => {
await expect(page).toMatchElement(
'div[class$=main] div[class$=headline] div[class$=titleEllipsis]',
{
await expect(page).toMatchElement('div[class$=main] div[class$=titleEllipsis]', {
text: title,
timeout: 2000,
}
);
});
};
export const expectToClickSidebarMenu = async (page: Page, menuText: string) => {
await expect(page).toClick('div[class$=sidebar] a div[class$=title]', {
text: menuText,
});
};

View file

@ -16,6 +16,7 @@ import guide from './guide.js';
import log_details from './log-details.js';
import logs from './logs.js';
import menu from './menu.js';
import mfa from './mfa.js';
import permissions from './permissions.js';
import profile from './profile.js';
import role_details from './role-details.js';
@ -73,6 +74,7 @@ const admin_console = {
subscription,
upsell,
guide,
mfa,
};
export default Object.freeze(admin_console);

View file

@ -0,0 +1,35 @@
const mfa = {
title: 'Multi-Faktor-Authentifizierung',
description:
'Fügen Sie der Sicherheit Ihres Anmeldeerlebnisses die Multi-Faktor-Authentifizierung hinzu.',
factors: 'Faktoren',
multi_factors: 'Multi-Faktoren',
multi_factors_description:
'Benutzer müssen einen der aktivierten Faktoren zur zweistufigen Authentifizierung überprüfen.',
totp: 'Authenticator-App OTP',
otp_description: 'Verknüpfen Sie Google Authenticator usw., um Einmalpasswörter zu überprüfen.',
webauthn: 'WebAuthn',
webauthn_description:
'WebAuthn verwendet den Passkey, um das Gerät des Benutzers zu überprüfen, einschließlich YubiKey.',
backup_code: 'Backup-Code',
backup_code_description:
'Generieren Sie 10 eindeutige Codes, von denen jeder für eine einzige Authentifizierung verwendet werden kann.',
backup_code_setup_hint:
'Der Backup-Authentifizierungsfaktor, der nicht alleine aktiviert werden kann:',
backup_code_error_hint:
'Um den Backup-Code für MFA zu verwenden, müssen andere Faktoren aktiviert sein, um die erfolgreiche Anmeldung Ihrer Benutzer sicherzustellen.',
policy: 'Richtlinie',
two_step_sign_in_policy: 'Zwei-Schritt-Authentifizierungsrichtlinie bei der Anmeldung',
two_step_sign_in_policy_description:
'Definieren Sie eine app-weite Anforderung für die zweistufige Authentifizierung bei der Anmeldung.',
user_controlled: 'Benutzerkontrolliert',
user_controlled_description:
'Standardmäßig deaktiviert und nicht obligatorisch, aber Benutzer können es individuell aktivieren.',
mandatory: 'Obligatorisch',
mandatory_description: 'Erfordern Sie MFA für alle Ihre Benutzer bei jeder Anmeldung.',
unlock_reminder:
'Entsperren Sie die MFA zur Sicherheitsüberprüfung durch ein Upgrade auf einen kostenpflichtigen Plan. Zögern Sie nicht, uns zu <a>kontaktieren</a>, wenn Sie Unterstützung benötigen.',
view_plans: 'Pläne anzeigen',
};
export default Object.freeze(mfa);

View file

@ -11,6 +11,8 @@ const tabs = {
roles: 'Rollen',
docs: 'Dokumentation',
tenant_settings: 'Einstellungen',
/** UNTRANSLATED */
mfa: 'Multi-factor auth',
};
export default Object.freeze(tabs);

View file

@ -16,6 +16,7 @@ import guide from './guide.js';
import log_details from './log-details.js';
import logs from './logs.js';
import menu from './menu.js';
import mfa from './mfa.js';
import permissions from './permissions.js';
import profile from './profile.js';
import role_details from './role-details.js';
@ -73,6 +74,7 @@ const admin_console = {
subscription,
upsell,
guide,
mfa,
};
export default Object.freeze(admin_console);

View file

@ -0,0 +1,32 @@
const mfa = {
title: 'Multi-factor authentication',
description:
'Add multi-factor authentication to elevate the security of your sign-in experience.',
factors: 'Factors',
multi_factors: 'Multi-factors',
multi_factors_description:
'Users need to verify one of the enabled factors for two-step authentication.',
totp: 'Authenticator app OTP',
otp_description: 'Link Google Authenticator, etc., to verify one-time passwords.',
webauthn: 'WebAuthn',
webauthn_description: 'WebAuthn uses the passkey to verify the users device including YubiKey.',
backup_code: 'Backup code',
backup_code_description: 'Generate 10 unique codes, each usable for a single authentication.',
backup_code_setup_hint: 'The backup authentication factor which can not be enabled alone:',
backup_code_error_hint:
'To use Backup code for MFA, other factors must be turned on to ensure your users successful sign-in.',
policy: 'Policy',
two_step_sign_in_policy: 'Two-step authentication policy at sign-in',
two_step_sign_in_policy_description:
'Define a app-wide 2-step authentication requirement for sign-in.',
user_controlled: 'User-controlled',
user_controlled_description:
'Disabled by default and not mandatory, but users can enable it individually.',
mandatory: 'Mandatory',
mandatory_description: 'Require MFA for all your users at every sign-in.',
unlock_reminder:
'Unlock MFA to verification security by upgrading to a paid plan. Dont hesitate to <a>contact us</a> if you need any assistance.',
view_plans: 'View plans',
};
export default Object.freeze(mfa);

View file

@ -11,6 +11,7 @@ const tabs = {
roles: 'Roles',
docs: 'Docs',
tenant_settings: 'Settings',
mfa: 'Multi-factor auth',
};
export default Object.freeze(tabs);

View file

@ -16,6 +16,7 @@ import guide from './guide.js';
import log_details from './log-details.js';
import logs from './logs.js';
import menu from './menu.js';
import mfa from './mfa.js';
import permissions from './permissions.js';
import profile from './profile.js';
import role_details from './role-details.js';
@ -73,6 +74,7 @@ const admin_console = {
subscription,
upsell,
guide,
mfa,
};
export default Object.freeze(admin_console);

View file

@ -0,0 +1,36 @@
const mfa = {
title: 'Autenticación multifactor',
description:
'Agrega autenticación multifactor para elevar la seguridad de tu experiencia de inicio de sesión.',
factors: 'Factores',
multi_factors: 'Multifactores',
multi_factors_description:
'Los usuarios deben verificar uno de los factores habilitados para la autenticación de dos pasos.',
totp: 'OTP de la aplicación autenticadora',
otp_description: 'Vincula Google Authenticator, etc., para verificar contraseñas de un solo uso.',
webauthn: 'WebAuthn',
webauthn_description:
'WebAuthn utiliza la clave de paso para verificar el dispositivo del usuario, incluido YubiKey.',
backup_code: 'Código de respaldo',
backup_code_description:
'Genera 10 códigos únicos, cada uno utilizable para una sola autenticación.',
backup_code_setup_hint:
'El factor de autenticación de respaldo que no se puede habilitar por sí solo:',
backup_code_error_hint:
'Para usar el código de respaldo para la autenticación multifactor, deben estar habilitados otros factores para garantizar el inicio de sesión exitoso de tus usuarios.',
policy: 'Política',
two_step_sign_in_policy: 'Política de autenticación de dos pasos al iniciar sesión',
two_step_sign_in_policy_description:
'Define un requisito de autenticación de dos pasos en toda la aplicación al iniciar sesión.',
user_controlled: 'Controlado por el usuario',
user_controlled_description:
'Desactivado por defecto y no obligatorio, pero los usuarios pueden habilitarlo individualmente.',
mandatory: 'Obligatorio',
mandatory_description:
'Requiere autenticación multifactor para todos tus usuarios en cada inicio de sesión.',
unlock_reminder:
'Desbloquea la autenticación multifactor para mejorar la seguridad mediante la actualización a un plan de pago. No dudes en <a>contactarnos</a> si necesitas ayuda.',
view_plans: 'Ver planes',
};
export default Object.freeze(mfa);

View file

@ -11,6 +11,8 @@ const tabs = {
roles: 'Roles',
docs: 'Documentos',
tenant_settings: 'Configuraciones',
/** UNTRANSLATED */
mfa: 'Multi-factor auth',
};
export default Object.freeze(tabs);

View file

@ -16,6 +16,7 @@ import guide from './guide.js';
import log_details from './log-details.js';
import logs from './logs.js';
import menu from './menu.js';
import mfa from './mfa.js';
import permissions from './permissions.js';
import profile from './profile.js';
import role_details from './role-details.js';
@ -73,6 +74,7 @@ const admin_console = {
subscription,
upsell,
guide,
mfa,
};
export default Object.freeze(admin_console);

View file

@ -0,0 +1,37 @@
const mfa = {
title: 'Authentification multi-facteurs',
description:
'Ajoutez une authentification multi-facteurs pour renforcer la sécurité de votre expérience de connexion.',
factors: 'Facteurs',
multi_factors: 'Multi-facteurs',
multi_factors_description:
"Les utilisateurs doivent vérifier l'un des facteurs activés pour l'authentification à deux étapes.",
totp: "OTP de l'application Authenticator",
otp_description:
'Liez Google Authenticator, etc., pour vérifier les mots de passe à usage unique.',
webauthn: 'WebAuthn',
webauthn_description:
"WebAuthn utilise la clé de passe pour vérifier le périphérique de l'utilisateur, y compris YubiKey.",
backup_code: 'Code de secours',
backup_code_description:
'Générez 10 codes uniques, chacun utilisable pour une seule authentification.',
backup_code_setup_hint:
"Le facteur d'authentification de secours qui ne peut pas être activé seul :",
backup_code_error_hint:
"Pour utiliser le code de secours pour l'authentification multi-facteurs, d'autres facteurs doivent être activés pour garantir la réussite de la connexion de vos utilisateurs.",
policy: 'Politique',
two_step_sign_in_policy: "Politique d'authentification à deux étapes à la connexion",
two_step_sign_in_policy_description:
"Définissez une exigence d'authentification à deux étapes dans toute l'application lors de la connexion.",
user_controlled: "Contrôlé par l'utilisateur",
user_controlled_description:
"Désactivé par défaut et non obligatoire, mais les utilisateurs peuvent l'activer individuellement.",
mandatory: 'Obligatoire',
mandatory_description:
"Exigez l'authentification multi-facteurs pour tous vos utilisateurs à chaque connexion.",
unlock_reminder:
"Débloquez l'authentification multi-facteurs pour renforcer la sécurité en passant à un abonnement payant. N'hésitez pas à <a>nous contacter</a> si vous avez besoin d'aide.",
view_plans: 'Voir les plans',
};
export default Object.freeze(mfa);

View file

@ -11,6 +11,8 @@ const tabs = {
roles: 'Roles',
docs: 'Documentation',
tenant_settings: 'Paramètres',
/** UNTRANSLATED */
mfa: 'Multi-factor auth',
};
export default Object.freeze(tabs);

View file

@ -16,6 +16,7 @@ import guide from './guide.js';
import log_details from './log-details.js';
import logs from './logs.js';
import menu from './menu.js';
import mfa from './mfa.js';
import permissions from './permissions.js';
import profile from './profile.js';
import role_details from './role-details.js';
@ -73,6 +74,7 @@ const admin_console = {
subscription,
upsell,
guide,
mfa,
};
export default Object.freeze(admin_console);

View file

@ -0,0 +1,36 @@
const mfa = {
title: 'Autenticazione multi-fattore',
description:
"Aggiungi l'autenticazione multi-fattore per aumentare la sicurezza della tua esperienza di accesso.",
factors: 'Fattori',
multi_factors: 'Multi-fattori',
multi_factors_description:
"Gli utenti devono verificare uno dei fattori abilitati per l'autenticazione a due passaggi.",
totp: "OTP dell'app Authenticator",
otp_description: 'Collega Google Authenticator, ecc., per verificare le password monouso.',
webauthn: 'WebAuthn',
webauthn_description:
"WebAuthn utilizza la chiave di passaggio per verificare il dispositivo dell'utente, inclusa YubiKey.",
backup_code: 'Codice di backup',
backup_code_description:
'Genera 10 codici unici, ciascuno utilizzabile per una singola autenticazione.',
backup_code_setup_hint:
'Il fattore di autenticazione di backup che non può essere attivato da solo:',
backup_code_error_hint:
"Per utilizzare il codice di backup per l'autenticazione multi-fattore, è necessario attivare altri fattori per garantire il successo dell'accesso dei tuoi utenti.",
policy: 'Politica',
two_step_sign_in_policy: "Politica di autenticazione a due passaggi all'accesso",
two_step_sign_in_policy_description:
"Definisci un requisito di autenticazione a due passaggi per l'applicazione al momento dell'accesso.",
user_controlled: "Controllato dall'utente",
user_controlled_description:
'Disabilitato per impostazione predefinita e non obbligatorio, ma gli utenti possono attivarlo singolarmente.',
mandatory: 'Obbligatorio',
mandatory_description:
"Richiedi l'autenticazione multi-fattore per tutti i tuoi utenti ad ogni accesso.",
unlock_reminder:
"Sblocca l'autenticazione multi-fattore per verificare la sicurezza passando a un piano a pagamento. Non esitare a <a>contattarci</a> se hai bisogno di assistenza.",
view_plans: 'Visualizza i piani',
};
export default Object.freeze(mfa);

View file

@ -11,6 +11,8 @@ const tabs = {
roles: 'Ruoli',
docs: 'Documenti',
tenant_settings: 'Impostazioni',
/** UNTRANSLATED */
mfa: 'Multi-factor auth',
};
export default Object.freeze(tabs);

View file

@ -16,6 +16,7 @@ import guide from './guide.js';
import log_details from './log-details.js';
import logs from './logs.js';
import menu from './menu.js';
import mfa from './mfa.js';
import permissions from './permissions.js';
import profile from './profile.js';
import role_details from './role-details.js';
@ -73,6 +74,7 @@ const admin_console = {
subscription,
upsell,
guide,
mfa,
};
export default Object.freeze(admin_console);

View file

@ -0,0 +1,32 @@
const mfa = {
title: '多要素認証',
description:
'サインインエクスペリエンスのセキュリティを向上させるために、多要素認証を追加します。',
factors: '要因',
multi_factors: 'マルチファクター',
multi_factors_description:
'ユーザーは2段階認証のために有効になっている要因の1つを確認する必要があります。',
totp: 'AuthenticatorアプリOTP',
otp_description: 'Google Authenticatorなどをリンクしてワンタイムパスワードを確認します。',
webauthn: 'WebAuthn',
webauthn_description:
'WebAuthnはYubiKeyを含むユーザーのデバイスを確認するためにパスキーを使用します。',
backup_code: 'バックアップコード',
backup_code_description: '1回の認証に使用できる10個のユニークなコードを生成します。',
backup_code_setup_hint: '単独で有効化できないバックアップ認証要因:',
backup_code_error_hint:
'MFAのバックアップコードを使用するには、他の要因を有効にする必要があり、ユーザーのサインインが成功することを確認します。',
policy: 'ポリシー',
two_step_sign_in_policy: 'サインイン時の2段階認証ポリシー',
two_step_sign_in_policy_description: 'サインイン時のアプリ全体の2段階認証要件を定義します。',
user_controlled: 'ユーザーがコントロール',
user_controlled_description:
'デフォルトでは無効で、強制ではありませんが、ユーザーは個別に有効にできます。',
mandatory: '必須',
mandatory_description: 'すべてのユーザーに対してすべてのサインインでMFAが必要です。',
unlock_reminder:
'セキュリティの確認のためにMFAをロック解除して有料プランにアップグレードします。サポートが必要な場合はお気軽に<a>お問い合わせ</a>ください。',
view_plans: 'プランを表示',
};
export default Object.freeze(mfa);

View file

@ -11,6 +11,8 @@ const tabs = {
roles: '役割',
docs: 'ドキュメント',
tenant_settings: '設定',
/** UNTRANSLATED */
mfa: 'Multi-factor auth',
};
export default Object.freeze(tabs);

View file

@ -16,6 +16,7 @@ import guide from './guide.js';
import log_details from './log-details.js';
import logs from './logs.js';
import menu from './menu.js';
import mfa from './mfa.js';
import permissions from './permissions.js';
import profile from './profile.js';
import role_details from './role-details.js';
@ -73,6 +74,7 @@ const admin_console = {
subscription,
upsell,
guide,
mfa,
};
export default Object.freeze(admin_console);

View file

@ -0,0 +1,31 @@
const mfa = {
title: '다중 인증',
description: '로그인 경험의 보안을 강화하기 위해 다중 인증을 추가하세요.',
factors: '요소',
multi_factors: '다중 요소',
multi_factors_description:
'사용자는 두 단계 인증을 위해 활성화된 요소 중 하나를 확인해야 합니다.',
totp: 'Authenticator 앱 OTP',
otp_description: 'Google Authenticator 등을 연결하여 일회용 암호를 확인합니다.',
webauthn: 'WebAuthn',
webauthn_description:
'WebAuthn은 YubiKey를 포함한 사용자 장치를 확인하기 위해 패스키를 사용합니다.',
backup_code: '백업 코드',
backup_code_description: '한 번의 인증에 사용할 수 있는 고유한 10개의 코드를 생성합니다.',
backup_code_setup_hint: '독립적으로 활성화할 수 없는 백업 인증 요소:',
backup_code_error_hint:
'MFA에 백업 코드를 사용하려면 다른 요소도 활성화되어 있어야 하며 사용자의 로그인이 성공적으로 이루어지도록 합니다.',
policy: '정책',
two_step_sign_in_policy: '로그인 시 이중 인증 정책',
two_step_sign_in_policy_description: '로그인 시 앱 전체에서 이중 인증 요구 사항을 정의합니다.',
user_controlled: '사용자 제어',
user_controlled_description:
'기본적으로 비활성화되어 있으며 필수 사항은 아니지만 사용자는 개별적으로 활성화할 수 있습니다.',
mandatory: '필수',
mandatory_description: '모든 사용자에 대한 모든 로그인에서 MFA가 필요합니다.',
unlock_reminder:
'보안을 확인하려면 유료 플랜으로 업그레이드하여 MFA를 잠금 해제하십시오. 도움이 필요하면 언제든지 <a>문의하십시오</a>.',
view_plans: '플랜 보기',
};
export default Object.freeze(mfa);

View file

@ -11,6 +11,8 @@ const tabs = {
roles: '역할',
docs: '문서',
tenant_settings: '설정',
/** UNTRANSLATED */
mfa: 'Multi-factor auth',
};
export default Object.freeze(tabs);

View file

@ -16,6 +16,7 @@ import guide from './guide.js';
import log_details from './log-details.js';
import logs from './logs.js';
import menu from './menu.js';
import mfa from './mfa.js';
import permissions from './permissions.js';
import profile from './profile.js';
import role_details from './role-details.js';
@ -73,6 +74,7 @@ const admin_console = {
subscription,
upsell,
guide,
mfa,
};
export default Object.freeze(admin_console);

View file

@ -0,0 +1,36 @@
const mfa = {
title: 'Wieloczynnikowa autoryzacja',
description:
'Dodaj wieloczynnikową autoryzację, aby podnieść bezpieczeństwo swojego doświadczenia z logowaniem.',
factors: 'Czynniki',
multi_factors: 'Wieloczynniki',
multi_factors_description:
'Użytkownicy muszą zweryfikować jeden z włączonych czynników podczas autoryzacji dwuetapowej.',
totp: 'OTP z aplikacji Authenticator',
otp_description: 'Połącz Google Authenticator itp., aby zweryfikować jednorazowe hasła.',
webauthn: 'WebAuthn',
webauthn_description:
'WebAuthn używa klucza przechodzenia do weryfikacji urządzenia użytkownika, w tym YubiKey.',
backup_code: 'Kod zapasowy',
backup_code_description:
'Generuj 10 unikalnych kodów, z których każdy można użyć do jednej autoryzacji.',
backup_code_setup_hint:
'Czynnik autoryzacji zapasowej, który nie może być włączony samodzielnie:',
backup_code_error_hint:
'Aby używać kodu zapasowego do autoryzacji wieloczynnikowej, inne czynniki muszą być włączone, aby zapewnić udane logowanie użytkowników.',
policy: 'Polityka',
two_step_sign_in_policy: 'Polityka autoryzacji dwuetapowej podczas logowania',
two_step_sign_in_policy_description:
'Zdefiniuj wymaganie autoryzacji dwuetapowej na poziomie aplikacji podczas logowania.',
user_controlled: 'Kontrolowane przez użytkownika',
user_controlled_description:
'Domyślnie wyłączone i nieobowiązkowe, ale użytkownicy mogą włączyć je indywidualnie.',
mandatory: 'Obowiązkowe',
mandatory_description:
'Wymagaj autoryzacji wieloczynnikowej dla wszystkich użytkowników podczas każdego logowania.',
unlock_reminder:
'Odblokuj autoryzację wieloczynnikową, aby zwiększyć bezpieczeństwo, przechodząc na płatny plan. Nie wahaj się <a>skontaktować z nami</a>, jeśli potrzebujesz pomocy.',
view_plans: 'Zobacz plany',
};
export default Object.freeze(mfa);

View file

@ -11,6 +11,8 @@ const tabs = {
roles: 'Role',
docs: 'Dokumentacja',
tenant_settings: 'Ustawienia',
/** UNTRANSLATED */
mfa: 'Multi-factor auth',
};
export default Object.freeze(tabs);

View file

@ -16,6 +16,7 @@ import guide from './guide.js';
import log_details from './log-details.js';
import logs from './logs.js';
import menu from './menu.js';
import mfa from './mfa.js';
import permissions from './permissions.js';
import profile from './profile.js';
import role_details from './role-details.js';
@ -73,6 +74,7 @@ const admin_console = {
subscription,
upsell,
guide,
mfa,
};
export default Object.freeze(admin_console);

View file

@ -0,0 +1,35 @@
const mfa = {
title: 'Autenticação de vários fatores',
description:
'Adicione autenticação de vários fatores para elevar a segurança da sua experiência de login.',
factors: 'Fatores',
multi_factors: 'Múltiplos fatores',
multi_factors_description:
'Os usuários precisam verificar um dos fatores habilitados para autenticação de dois passos.',
totp: 'OTP do aplicativo Authenticator',
otp_description: 'Vincule o Google Authenticator, etc., para verificar senhas de uso único.',
webauthn: 'WebAuthn',
webauthn_description:
'O WebAuthn usa a chave de passagem para verificar o dispositivo do usuário, incluindo o YubiKey.',
backup_code: 'Código de backup',
backup_code_description:
'Gere 10 códigos exclusivos, cada um utilizável para uma única autenticação.',
backup_code_setup_hint: 'O fator de autenticação de backup que não pode ser ativado sozinho:',
backup_code_error_hint:
'Para usar o código de backup para autenticação de vários fatores, outros fatores devem estar ativados para garantir o login bem-sucedido de seus usuários.',
policy: 'Política',
two_step_sign_in_policy: 'Política de autenticação de dois passos no login',
two_step_sign_in_policy_description:
'Defina um requisito de autenticação de dois passos em toda a aplicação no momento do login.',
user_controlled: 'Controlado pelo usuário',
user_controlled_description:
'Desativado por padrão e não obrigatório, mas os usuários podem ativá-lo individualmente.',
mandatory: 'Obrigatório',
mandatory_description:
'Exija autenticação de vários fatores para todos os seus usuários em cada login.',
unlock_reminder:
'Desbloqueie a autenticação de vários fatores para verificar a segurança, fazendo upgrade para um plano pago. Não hesite em <a>entrar em contato conosco</a> se precisar de assistência.',
view_plans: 'Ver planos',
};
export default Object.freeze(mfa);

View file

@ -11,6 +11,8 @@ const tabs = {
roles: 'Registros',
docs: 'Documentação',
tenant_settings: 'Configurações',
/** UNTRANSLATED */
mfa: 'Multi-factor auth',
};
export default Object.freeze(tabs);

View file

@ -16,6 +16,7 @@ import guide from './guide.js';
import log_details from './log-details.js';
import logs from './logs.js';
import menu from './menu.js';
import mfa from './mfa.js';
import permissions from './permissions.js';
import profile from './profile.js';
import role_details from './role-details.js';
@ -73,6 +74,7 @@ const admin_console = {
subscription,
upsell,
guide,
mfa,
};
export default Object.freeze(admin_console);

View file

@ -0,0 +1,35 @@
const mfa = {
title: 'Autenticação de vários fatores',
description:
'Adicione autenticação de vários fatores para elevar a segurança da sua experiência de login.',
factors: 'Fatores',
multi_factors: 'Múltiplos fatores',
multi_factors_description:
'Os usuários precisam verificar um dos fatores habilitados para autenticação de dois passos.',
totp: 'OTP do aplicativo Authenticator',
otp_description: 'Vincule o Google Authenticator, etc., para verificar senhas de uso único.',
webauthn: 'WebAuthn',
webauthn_description:
'O WebAuthn usa a chave de passagem para verificar o dispositivo do usuário, incluindo o YubiKey.',
backup_code: 'Código de backup',
backup_code_description:
'Gere 10 códigos exclusivos, cada um utilizável para uma única autenticação.',
backup_code_setup_hint: 'O fator de autenticação de backup que não pode ser ativado sozinho:',
backup_code_error_hint:
'Para usar o código de backup para autenticação de vários fatores, outros fatores devem estar ativados para garantir o login bem-sucedido de seus usuários.',
policy: 'Política',
two_step_sign_in_policy: 'Política de autenticação de dois passos no login',
two_step_sign_in_policy_description:
'Defina um requisito de autenticação de dois passos em toda a aplicação no momento do login.',
user_controlled: 'Controlado pelo usuário',
user_controlled_description:
'Desativado por padrão e não obrigatório, mas os usuários podem ativá-lo individualmente.',
mandatory: 'Obrigatório',
mandatory_description:
'Exija autenticação de vários fatores para todos os seus usuários em cada login.',
unlock_reminder:
'Desbloqueie a autenticação de vários fatores para verificar a segurança, fazendo upgrade para um plano pago. Não hesite em <a>entrar em contato conosco</a> se precisar de assistência.',
view_plans: 'Ver planos',
};
export default Object.freeze(mfa);

View file

@ -11,6 +11,8 @@ const tabs = {
roles: 'Papéis',
docs: 'Documentação',
tenant_settings: 'Definições',
/** UNTRANSLATED */
mfa: 'Multi-factor auth',
};
export default Object.freeze(tabs);

View file

@ -16,6 +16,7 @@ import guide from './guide.js';
import log_details from './log-details.js';
import logs from './logs.js';
import menu from './menu.js';
import mfa from './mfa.js';
import permissions from './permissions.js';
import profile from './profile.js';
import role_details from './role-details.js';
@ -73,6 +74,7 @@ const admin_console = {
subscription,
upsell,
guide,
mfa,
};
export default Object.freeze(admin_console);

View file

@ -0,0 +1,35 @@
const mfa = {
title: 'Многофакторная аутентификация',
description:
'Добавьте многофакторную аутентификацию для повышения безопасности вашего опыта входа.',
factors: 'Факторы',
multi_factors: 'Многофакторы',
multi_factors_description:
'Пользователи должны проверить один из включенных факторов для двухэтапной аутентификации.',
totp: 'OTP из приложения Authenticator',
otp_description: 'Свяжите Google Authenticator и т. д., чтобы проверить одноразовые пароли.',
webauthn: 'WebAuthn',
webauthn_description:
'WebAuthn использует ключ прохода для проверки устройства пользователя, включая YubiKey.',
backup_code: 'Резервный код',
backup_code_description:
'Генерируйте 10 уникальных кодов, каждый из которых можно использовать для одной аутентификации.',
backup_code_setup_hint: 'Фактор резервной аутентификации, который нельзя включить отдельно:',
backup_code_error_hint:
'Чтобы использовать резервный код для многофакторной аутентификации, другие факторы должны быть включены для обеспечения успешного входа ваших пользователей.',
policy: 'Политика',
two_step_sign_in_policy: 'Политика двухэтапной аутентификации при входе',
two_step_sign_in_policy_description:
'Задайте требование двухэтапной аутентификации для всего приложения при входе.',
user_controlled: 'Управление пользователем',
user_controlled_description:
'По умолчанию отключено и не обязательно, но пользователи могут включить его по отдельности.',
mandatory: 'Обязательно',
mandatory_description:
'Требуйте многофакторную аутентификацию для всех ваших пользователей при каждом входе.',
unlock_reminder:
'Разблокируйте многофакторную аутентификацию для увеличения безопасности с помощью перехода на платный план. Не стесняйтесь <a>связаться с нами</a>, если вам нужна помощь.',
view_plans: 'Просмотреть планы',
};
export default Object.freeze(mfa);

View file

@ -11,6 +11,8 @@ const tabs = {
roles: 'Роли',
docs: 'Документация',
tenant_settings: 'Настройки',
/** UNTRANSLATED */
mfa: 'Multi-factor auth',
};
export default Object.freeze(tabs);

View file

@ -16,6 +16,7 @@ import guide from './guide.js';
import log_details from './log-details.js';
import logs from './logs.js';
import menu from './menu.js';
import mfa from './mfa.js';
import permissions from './permissions.js';
import profile from './profile.js';
import role_details from './role-details.js';
@ -73,6 +74,7 @@ const admin_console = {
subscription,
upsell,
guide,
mfa,
};
export default Object.freeze(admin_console);

View file

@ -0,0 +1,36 @@
const mfa = {
title: 'Çoklu Faktör Kimlik Doğrulama',
description:
'Giriş deneyiminizin güvenliğini artırmak için çoklu faktör kimlik doğrulamayı ekleyin.',
factors: 'Faktörler',
multi_factors: 'Çoklu faktörler',
multi_factors_description:
'Kullanıcılar, iki aşamalı kimlik doğrulama için etkinleştirilen faktörlerden birini doğrulamalıdır.',
totp: 'Authenticator uygulama OTP',
otp_description:
'Google Authenticator vb. bağlayarak tek kullanımlık şifreleri doğrulamak için kullanın.',
webauthn: 'WebAuthn',
webauthn_description:
'WebAuthn, YubiKey dahil olmak üzere kullanıcının cihazını doğrulamak için geçiş anahtarını kullanır.',
backup_code: 'Yedek kod',
backup_code_description:
'Tek kullanımlık bir kimlik doğrulama için kullanılabilen 10 benzersiz kod üretin.',
backup_code_setup_hint: 'Tek başına etkinleştirilemeyen yedek kimlik doğrulama faktörü:',
backup_code_error_hint:
'Çoklu faktör kimlik doğrulamada yedek kodu kullanmak için kullanıcılarınızın başarılı giriş yapmalarını sağlamak için diğer faktörlerin etkinleştirilmiş olması gerekir.',
policy: 'Politika',
two_step_sign_in_policy: 'Giriş sırasında iki adımlı kimlik doğrulama politikası',
two_step_sign_in_policy_description:
'Giriş sırasında tüm uygulama genelinde iki adımlı kimlik doğrulama gereksinimi tanımlayın.',
user_controlled: 'Kullanıcı tarafından kontrol edilen',
user_controlled_description:
'Varsayılan olarak devre dışı bırakılmış ve zorunlu değildir, ancak kullanıcılar ayrı ayrı etkinleştirebilirler.',
mandatory: 'Zorunlu',
mandatory_description:
'Her girişte tüm kullanıcılarınız için çoklu faktör kimlik doğrulamayı gerektirin.',
unlock_reminder:
'Güvenliği doğrulamak için çoklu faktör kimlik doğrulamayı kilit açmak için bir ücretli plana yükselterek etkinleştirin. Yardıma ihtiyacınız varsa çekinmeden <a>bizimle iletişime geçin</a>.',
view_plans: 'Planları görüntüle',
};
export default Object.freeze(mfa);

View file

@ -11,6 +11,8 @@ const tabs = {
roles: 'Roller',
docs: 'Dökümanlar',
tenant_settings: 'Ayarlar',
/** UNTRANSLATED */
mfa: 'Multi-factor auth',
};
export default Object.freeze(tabs);

View file

@ -16,6 +16,7 @@ import guide from './guide.js';
import log_details from './log-details.js';
import logs from './logs.js';
import menu from './menu.js';
import mfa from './mfa.js';
import permissions from './permissions.js';
import profile from './profile.js';
import role_details from './role-details.js';
@ -73,6 +74,7 @@ const admin_console = {
subscription,
upsell,
guide,
mfa,
};
export default Object.freeze(admin_console);

View file

@ -0,0 +1,27 @@
const mfa = {
title: '多因素身份验证',
description: '添加多因素身份验证以提升您的登录体验的安全性。',
factors: '因素',
multi_factors: '多因素',
multi_factors_description: '用户需要验证启用的一个因素以进行两步验证。',
totp: 'Authenticator应用程序OTP',
otp_description: '链接Google Authenticator等来验证一次性密码。',
webauthn: 'WebAuthn',
webauthn_description: 'WebAuthn使用通行密钥验证用户设备包括YubiKey。',
backup_code: '备用代码',
backup_code_description: '生成10个唯一的代码每个代码可用于一次验证。',
backup_code_setup_hint: '不能单独启用的备用身份验证因素:',
backup_code_error_hint: '要使用备用代码进行多因素身份验证,必须启用其他因素以确保用户成功登录。',
policy: '策略',
two_step_sign_in_policy: '登录时的两步验证策略',
two_step_sign_in_policy_description: '为登录时的应用程序定义双重验证要求。',
user_controlled: '用户控制',
user_controlled_description: '默认情况下禁用且非强制,但用户可以单独启用它。',
mandatory: '强制',
mandatory_description: '要求所有用户在每次登录时进行多因素身份验证。',
unlock_reminder:
'解锁多因素身份验证以通过升级到付费计划验证安全性。如果需要帮助,请随时<a>联系我们</a>。',
view_plans: '查看计划',
};
export default Object.freeze(mfa);

View file

@ -11,6 +11,8 @@ const tabs = {
roles: '角色',
docs: '文档',
tenant_settings: '设置',
/** UNTRANSLATED */
mfa: 'Multi-factor auth',
};
export default Object.freeze(tabs);

View file

@ -16,6 +16,7 @@ import guide from './guide.js';
import log_details from './log-details.js';
import logs from './logs.js';
import menu from './menu.js';
import mfa from './mfa.js';
import permissions from './permissions.js';
import profile from './profile.js';
import role_details from './role-details.js';
@ -73,6 +74,7 @@ const admin_console = {
subscription,
upsell,
guide,
mfa,
};
export default Object.freeze(admin_console);

View file

@ -0,0 +1,27 @@
const mfa = {
title: '多因子驗證',
description: '添加多因子驗證以提升您的登錄體驗的安全性。',
factors: '因素',
multi_factors: '多因素',
multi_factors_description: '用戶需要驗證啟用的一個因素以進行兩步驗證。',
totp: 'Authenticator應用程式OTP',
otp_description: '連接Google Authenticator等來驗證一次性密碼。',
webauthn: 'WebAuthn',
webauthn_description: 'WebAuthn使用通行密鑰驗證用戶設備包括YubiKey。',
backup_code: '備用代碼',
backup_code_description: '生成10個唯一的代碼每個代碼可用於一次驗證。',
backup_code_setup_hint: '不能單獨啟用的備用身份驗證因素:',
backup_code_error_hint: '要使用備用代碼進行多因子驗證,必須啟用其他因素以確保用戶成功登錄。',
policy: '策略',
two_step_sign_in_policy: '登錄時的雙重驗證策略',
two_step_sign_in_policy_description: '為登錄時的應用程序定義雙重驗證要求。',
user_controlled: '用戶控制',
user_controlled_description: '默認情況下禁用且非強制,但用戶可以單獨啟用它。',
mandatory: '強制',
mandatory_description: '要求所有用戶在每次登錄時進行多因子驗證。',
unlock_reminder:
'解鎖多因子驗證以通過升級到付費計劃驗證安全性。如果需要幫助,請隨時<a>聯繫我們</a>。',
view_plans: '查看計劃',
};
export default Object.freeze(mfa);

View file

@ -11,6 +11,8 @@ const tabs = {
roles: '角色',
docs: '文檔',
tenant_settings: '設置',
/** UNTRANSLATED */
mfa: 'Multi-factor auth',
};
export default Object.freeze(tabs);

View file

@ -16,6 +16,7 @@ import guide from './guide.js';
import log_details from './log-details.js';
import logs from './logs.js';
import menu from './menu.js';
import mfa from './mfa.js';
import permissions from './permissions.js';
import profile from './profile.js';
import role_details from './role-details.js';
@ -73,6 +74,7 @@ const admin_console = {
subscription,
upsell,
guide,
mfa,
};
export default Object.freeze(admin_console);

View file

@ -0,0 +1,27 @@
const mfa = {
title: '多因素驗證',
description: '添加多因素驗證以提升您的登錄體驗的安全性。',
factors: '因素',
multi_factors: '多因素',
multi_factors_description: '用戶需要驗證啟用的一個因素以進行兩步驗證。',
totp: 'Authenticator應用程式OTP',
otp_description: '連接Google Authenticator等來驗證一次性密碼。',
webauthn: 'WebAuthn',
webauthn_description: 'WebAuthn使用通行密鑰驗證用戶設備包括YubiKey。',
backup_code: '備用代碼',
backup_code_description: '生成10個唯一的代碼每個代碼可用於一次驗證。',
backup_code_setup_hint: '不能單獨啟用的備用身份驗證因素:',
backup_code_error_hint: '要使用備用代碼進行多因子驗證,必須啟用其他因素以確保用戶成功登錄。',
policy: '策略',
two_step_sign_in_policy: '登錄時的雙重驗證策略',
two_step_sign_in_policy_description: '為登錄時的應用程序定義雙重驗證要求。',
user_controlled: '用戶控制',
user_controlled_description: '默認情況下禁用且非強制,但用戶可以單獨啟用它。',
mandatory: '強制',
mandatory_description: '要求所有用戶在每次登錄時進行多因子驗證。',
unlock_reminder:
'解鎖多因子驗證以通過升級到付費計劃驗證安全性。如果需要幫助,請隨時<a>聯繫我們</a>。',
view_plans: '查看計劃',
};
export default Object.freeze(mfa);

View file

@ -11,6 +11,8 @@ const tabs = {
roles: '角色',
docs: '文件',
tenant_settings: '設定',
/** UNTRANSLATED */
mfa: 'Multi-factor auth',
};
export default Object.freeze(tabs);