Files
2025-02-19 16:36:29 -05:00

589 lines
83 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="max-image-preview:large">
<title>Roll your own authenticator app with KeystoneJS and React &#8211; pt 2 &#8211; hackanooga</title>
<link rel="alternate" type="application/rss+xml" title="hackanooga &raquo; Feed" href="/feed/">
<link rel="alternate" type="application/rss+xml" title="hackanooga &raquo; Comments Feed" href="/comments/feed/">
<link rel="alternate" type="application/rss+xml" title="hackanooga &raquo; Roll your own authenticator app with KeystoneJS and React &#8211; pt 2 Comments Feed" href="/roll-your-own-authenticator-app-with-keystonejs-and-react-pt-2/feed/">
<script>
window._wpemojiSettings = {"baseUrl":"https:\/\/s.w.org\/images\/core\/emoji\/15.0.3\/72x72\/","ext":".png","svgUrl":"https:\/\/s.w.org\/images\/core\/emoji\/15.0.3\/svg\/","svgExt":".svg","source":{"concatemoji":"\/wp-includes\/js\/wp-emoji-release.min.js?ver=6.6.2"}};
/*! This file is auto-generated */
!function(i,n){var o,s,e;function c(e){try{var t={supportTests:e,timestamp:(new Date).valueOf()};sessionStorage.setItem(o,JSON.stringify(t))}catch(e){}}function p(e,t,n){e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(t,0,0);var t=new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data),r=(e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(n,0,0),new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data));return t.every(function(e,t){return e===r[t]})}function u(e,t,n){switch(t){case"flag":return n(e,"🏳️‍⚧️","🏳️​⚧️")?!1:!n(e,"🇺🇳","🇺​🇳")&&!n(e,"🏴󠁧󠁢󠁥󠁮󠁧󠁿","🏴​󠁧​󠁢​󠁥​󠁮​󠁧​󠁿");case"emoji":return!n(e,"🐦‍⬛","🐦​⬛")}return!1}function f(e,t,n){var r="undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?new OffscreenCanvas(300,150):i.createElement("canvas"),a=r.getContext("2d",{willReadFrequently:!0}),o=(a.textBaseline="top",a.font="600 32px Arial",{});return e.forEach(function(e){o[e]=t(a,e,n)}),o}function t(e){var t=i.createElement("script");t.src=e,t.defer=!0,i.head.appendChild(t)}"undefined"!=typeof Promise&&(o="wpEmojiSettingsSupports",s=["flag","emoji"],n.supports={everything:!0,everythingExceptFlag:!0},e=new Promise(function(e){i.addEventListener("DOMContentLoaded",e,{once:!0})}),new Promise(function(t){var n=function(){try{var e=JSON.parse(sessionStorage.getItem(o));if("object"==typeof e&&"number"==typeof e.timestamp&&(new Date).valueOf()<e.timestamp+604800&&"object"==typeof e.supportTests)return e.supportTests}catch(e){}return null}();if(!n){if("undefined"!=typeof Worker&&"undefined"!=typeof OffscreenCanvas&&"undefined"!=typeof URL&&URL.createObjectURL&&"undefined"!=typeof Blob)try{var e="postMessage("+f.toString()+"("+[JSON.stringify(s),u.toString(),p.toString()].join(",")+"));",r=new Blob([e],{type:"text/javascript"}),a=new Worker(URL.createObjectURL(r),{name:"wpTestEmojiSupports"});return void(a.onmessage=function(e){c(n=e.data),a.terminate(),t(n)})}catch(e){}c(n=f(s,u,p))}t(n)}).then(function(e){for(var t in e)n.supports[t]=e[t],n.supports.everything=n.supports.everything&&n.supports[t],"flag"!==t&&(n.supports.everythingExceptFlag=n.supports.everythingExceptFlag&&n.supports[t]);n.supports.everythingExceptFlag=n.supports.everythingExceptFlag&&!n.supports.flag,n.DOMReady=!1,n.readyCallback=function(){n.DOMReady=!0}}).then(function(){return e}).then(function(){var e;n.supports.everything||(n.readyCallback(),(e=n.source||{}).concatemoji?t(e.concatemoji):e.wpemoji&&e.twemoji&&(t(e.twemoji),t(e.wpemoji)))}))}((window,document),window._wpemojiSettings);
</script>
<style id="wp-block-site-title-inline-css">:root :where(.wp-block-site-title a){color:inherit}</style>
<style id="wp-block-group-inline-css">.wp-block-group{box-sizing:border-box}:where(.wp-block-group.wp-block-group-is-layout-constrained){position:relative}</style>
<style id="wp-block-navigation-link-inline-css">.wp-block-navigation .wp-block-navigation-item__label{overflow-wrap:break-word}.wp-block-navigation .wp-block-navigation-item__description{display:none}.link-ui-tools{border-top:1px solid #f0f0f0;padding:8px}.link-ui-block-inserter{padding-top:8px}.link-ui-block-inserter__back{margin-left:8px;text-transform:uppercase}</style>
<link rel="stylesheet" id="wp-block-navigation-css" href="/wp-includes/blocks/navigation/style.min.css?ver=6.6.2" media="all">
<link rel="stylesheet" id="wp-block-social-links-css" href="/wp-includes/blocks/social-links/style.min.css?ver=6.6.2" media="all">
<style id="wp-block-post-date-inline-css">.wp-block-post-date{box-sizing:border-box}</style>
<style id="wp-block-post-title-inline-css">.wp-block-post-title{box-sizing:border-box;word-break:break-word}.wp-block-post-title a{display:inline-block}</style>
<style id="wp-block-post-featured-image-inline-css">.wp-block-post-featured-image{margin-left:0;margin-right:0}.wp-block-post-featured-image a{display:block;height:100%}.wp-block-post-featured-image :where(img){box-sizing:border-box;height:auto;max-width:100%;vertical-align:bottom;width:100%}.wp-block-post-featured-image.alignfull img,.wp-block-post-featured-image.alignwide img{width:100%}.wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim{background-color:#000;inset:0;position:absolute}.wp-block-post-featured-image{position:relative}.wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-gradient{background-color:initial}.wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim-0{opacity:0}.wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim-10{opacity:.1}.wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim-20{opacity:.2}.wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim-30{opacity:.3}.wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim-40{opacity:.4}.wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim-50{opacity:.5}.wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim-60{opacity:.6}.wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim-70{opacity:.7}.wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim-80{opacity:.8}.wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim-90{opacity:.9}.wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim-100{opacity:1}.wp-block-post-featured-image:where(.alignleft,.alignright){width:100%}</style>
<style id="wp-block-paragraph-inline-css">.is-small-text{font-size:.875em}.is-regular-text{font-size:1em}.is-large-text{font-size:2.25em}.is-larger-text{font-size:3em}.has-drop-cap:not(:focus):first-letter{float:left;font-size:8.4em;font-style:normal;font-weight:100;line-height:.68;margin:.05em .1em 0 0;text-transform:uppercase}body.rtl .has-drop-cap:not(:focus):first-letter{float:none;margin-left:.1em}p.has-drop-cap.has-background{overflow:hidden}:root :where(p.has-background){padding:1.25em 2.375em}:where(p.has-text-color:not(.has-link-color)) a{color:inherit}p.has-text-align-left[style*="writing-mode:vertical-lr"],p.has-text-align-right[style*="writing-mode:vertical-rl"]{rotate:180deg}</style>
<style id="wp-block-code-inline-css">.wp-block-code{box-sizing:border-box}.wp-block-code code{display:block;font-family:inherit;overflow-wrap:break-word;white-space:pre-wrap}</style>
<link rel="stylesheet" id="wp-block-image-css" href="/wp-includes/blocks/image/style.min.css?ver=6.6.2" media="all">
<style id="wp-block-post-terms-inline-css">.wp-block-post-terms{box-sizing:border-box}.wp-block-post-terms .wp-block-post-terms__separator{white-space:pre-wrap}</style>
<style id="wp-block-comment-template-inline-css">.wp-block-comment-template{box-sizing:border-box;list-style:none;margin-bottom:0;max-width:100%;padding:0}.wp-block-comment-template li{clear:both}.wp-block-comment-template ol{list-style:none;margin-bottom:0;max-width:100%;padding-left:2rem}.wp-block-comment-template.alignleft{float:left}.wp-block-comment-template.aligncenter{margin-left:auto;margin-right:auto;width:-moz-fit-content;width:fit-content}.wp-block-comment-template.alignright{float:right}</style>
<style id="wp-block-comments-pagination-inline-css">.wp-block-comments-pagination>.wp-block-comments-pagination-next,.wp-block-comments-pagination>.wp-block-comments-pagination-numbers,.wp-block-comments-pagination>.wp-block-comments-pagination-previous{margin-bottom:.5em;margin-right:.5em}.wp-block-comments-pagination>.wp-block-comments-pagination-next:last-child,.wp-block-comments-pagination>.wp-block-comments-pagination-numbers:last-child,.wp-block-comments-pagination>.wp-block-comments-pagination-previous:last-child{margin-right:0}.wp-block-comments-pagination .wp-block-comments-pagination-previous-arrow{display:inline-block;margin-right:1ch}.wp-block-comments-pagination .wp-block-comments-pagination-previous-arrow:not(.is-arrow-chevron){transform:scaleX(1)}.wp-block-comments-pagination .wp-block-comments-pagination-next-arrow{display:inline-block;margin-left:1ch}.wp-block-comments-pagination .wp-block-comments-pagination-next-arrow:not(.is-arrow-chevron){transform:scaleX(1)}.wp-block-comments-pagination.aligncenter{justify-content:center}</style>
<style id="wp-block-post-comments-form-inline-css">:where(.wp-block-post-comments-form) input:not([type=submit]),:where(.wp-block-post-comments-form) textarea{border:1px solid #949494;font-family:inherit;font-size:1em}:where(.wp-block-post-comments-form) input:where(:not([type=submit]):not([type=checkbox])),:where(.wp-block-post-comments-form) textarea{padding:calc(.667em + 2px)}.wp-block-post-comments-form{box-sizing:border-box}.wp-block-post-comments-form[style*=font-weight] :where(.comment-reply-title){font-weight:inherit}.wp-block-post-comments-form[style*=font-family] :where(.comment-reply-title){font-family:inherit}.wp-block-post-comments-form[class*=-font-size] :where(.comment-reply-title),.wp-block-post-comments-form[style*=font-size] :where(.comment-reply-title){font-size:inherit}.wp-block-post-comments-form[style*=line-height] :where(.comment-reply-title){line-height:inherit}.wp-block-post-comments-form[style*=font-style] :where(.comment-reply-title){font-style:inherit}.wp-block-post-comments-form[style*=letter-spacing] :where(.comment-reply-title){letter-spacing:inherit}.wp-block-post-comments-form input[type=submit]{box-shadow:none;cursor:pointer;display:inline-block;overflow-wrap:break-word;text-align:center}.wp-block-post-comments-form .comment-form input:not([type=submit]):not([type=checkbox]):not([type=hidden]),.wp-block-post-comments-form .comment-form textarea{box-sizing:border-box;display:block;width:100%}.wp-block-post-comments-form .comment-form-author label,.wp-block-post-comments-form .comment-form-email label,.wp-block-post-comments-form .comment-form-url label{display:block;margin-bottom:.25em}.wp-block-post-comments-form .comment-form-cookies-consent{display:flex;gap:.25em}.wp-block-post-comments-form .comment-form-cookies-consent #wp-comment-cookies-consent{margin-top:.35em}.wp-block-post-comments-form .comment-reply-title{margin-bottom:0}.wp-block-post-comments-form .comment-reply-title :where(small){font-size:var(--wp--preset--font-size--medium,smaller);margin-left:.5em}</style>
<style id="wp-block-buttons-inline-css">.wp-block-buttons.is-vertical{flex-direction:column}.wp-block-buttons.is-vertical>.wp-block-button:last-child{margin-bottom:0}.wp-block-buttons>.wp-block-button{display:inline-block;margin:0}.wp-block-buttons.is-content-justification-left{justify-content:flex-start}.wp-block-buttons.is-content-justification-left.is-vertical{align-items:flex-start}.wp-block-buttons.is-content-justification-center{justify-content:center}.wp-block-buttons.is-content-justification-center.is-vertical{align-items:center}.wp-block-buttons.is-content-justification-right{justify-content:flex-end}.wp-block-buttons.is-content-justification-right.is-vertical{align-items:flex-end}.wp-block-buttons.is-content-justification-space-between{justify-content:space-between}.wp-block-buttons.aligncenter{text-align:center}.wp-block-buttons:not(.is-content-justification-space-between,.is-content-justification-right,.is-content-justification-left,.is-content-justification-center) .wp-block-button.aligncenter{margin-left:auto;margin-right:auto;width:100%}.wp-block-buttons[style*=text-decoration] .wp-block-button,.wp-block-buttons[style*=text-decoration] .wp-block-button__link{text-decoration:inherit}.wp-block-buttons.has-custom-font-size .wp-block-button__link{font-size:inherit}.wp-block-button.aligncenter{text-align:center}</style>
<style id="wp-block-button-inline-css">.wp-block-button__link{box-sizing:border-box;cursor:pointer;display:inline-block;text-align:center;word-break:break-word}.wp-block-button__link.aligncenter{text-align:center}.wp-block-button__link.alignright{text-align:right}:where(.wp-block-button__link){border-radius:9999px;box-shadow:none;padding:calc(.667em + 2px) calc(1.333em + 2px);text-decoration:none}.wp-block-button[style*=text-decoration] .wp-block-button__link{text-decoration:inherit}.wp-block-buttons>.wp-block-button.has-custom-width{max-width:none}.wp-block-buttons>.wp-block-button.has-custom-width .wp-block-button__link{width:100%}.wp-block-buttons>.wp-block-button.has-custom-font-size .wp-block-button__link{font-size:inherit}.wp-block-buttons>.wp-block-button.wp-block-button__width-25{width:calc(25% - var(--wp--style--block-gap, .5em)*.75)}.wp-block-buttons>.wp-block-button.wp-block-button__width-50{width:calc(50% - var(--wp--style--block-gap, .5em)*.5)}.wp-block-buttons>.wp-block-button.wp-block-button__width-75{width:calc(75% - var(--wp--style--block-gap, .5em)*.25)}.wp-block-buttons>.wp-block-button.wp-block-button__width-100{flex-basis:100%;width:100%}.wp-block-buttons.is-vertical>.wp-block-button.wp-block-button__width-25{width:25%}.wp-block-buttons.is-vertical>.wp-block-button.wp-block-button__width-50{width:50%}.wp-block-buttons.is-vertical>.wp-block-button.wp-block-button__width-75{width:75%}.wp-block-button.is-style-squared,.wp-block-button__link.wp-block-button.is-style-squared{border-radius:0}.wp-block-button.no-border-radius,.wp-block-button__link.no-border-radius{border-radius:0!important}:root :where(.wp-block-button .wp-block-button__link.is-style-outline),:root :where(.wp-block-button.is-style-outline>.wp-block-button__link){border:2px solid;padding:.667em 1.333em}:root :where(.wp-block-button .wp-block-button__link.is-style-outline:not(.has-text-color)),:root :where(.wp-block-button.is-style-outline>.wp-block-button__link:not(.has-text-color)){color:currentColor}:root :where(.wp-block-button .wp-block-button__link.is-style-outline:not(.has-background)),:root :where(.wp-block-button.is-style-outline>.wp-block-button__link:not(.has-background)){background-color:initial;background-image:none}</style>
<style id="wp-block-separator-inline-css">@charset "UTF-8";.wp-block-separator{border:none;border-top:2px solid}:root :where(.wp-block-separator.is-style-dots){height:auto;line-height:1;text-align:center}:root :where(.wp-block-separator.is-style-dots):before{color:currentColor;content:"···";font-family:serif;font-size:1.5em;letter-spacing:2em;padding-left:2em}.wp-block-separator.is-style-dots{background:none!important;border:none!important}</style>
<style id="wp-block-post-navigation-link-inline-css">.wp-block-post-navigation-link .wp-block-post-navigation-link__arrow-previous{display:inline-block;margin-right:1ch}.wp-block-post-navigation-link .wp-block-post-navigation-link__arrow-previous:not(.is-arrow-chevron){transform:scaleX(1)}.wp-block-post-navigation-link .wp-block-post-navigation-link__arrow-next{display:inline-block;margin-left:1ch}.wp-block-post-navigation-link .wp-block-post-navigation-link__arrow-next:not(.is-arrow-chevron){transform:scaleX(1)}.wp-block-post-navigation-link.has-text-align-left[style*="writing-mode: vertical-lr"],.wp-block-post-navigation-link.has-text-align-right[style*="writing-mode: vertical-rl"]{rotate:180deg}</style>
<style id="wp-block-query-pagination-inline-css">.wp-block-query-pagination>.wp-block-query-pagination-next,.wp-block-query-pagination>.wp-block-query-pagination-numbers,.wp-block-query-pagination>.wp-block-query-pagination-previous{margin-bottom:.5em;margin-right:.5em}.wp-block-query-pagination>.wp-block-query-pagination-next:last-child,.wp-block-query-pagination>.wp-block-query-pagination-numbers:last-child,.wp-block-query-pagination>.wp-block-query-pagination-previous:last-child{margin-right:0}.wp-block-query-pagination.is-content-justification-space-between>.wp-block-query-pagination-next:last-of-type{margin-inline-start:auto}.wp-block-query-pagination.is-content-justification-space-between>.wp-block-query-pagination-previous:first-child{margin-inline-end:auto}.wp-block-query-pagination .wp-block-query-pagination-previous-arrow{display:inline-block;margin-right:1ch}.wp-block-query-pagination .wp-block-query-pagination-previous-arrow:not(.is-arrow-chevron){transform:scaleX(1)}.wp-block-query-pagination .wp-block-query-pagination-next-arrow{display:inline-block;margin-left:1ch}.wp-block-query-pagination .wp-block-query-pagination-next-arrow:not(.is-arrow-chevron){transform:scaleX(1)}.wp-block-query-pagination.aligncenter{justify-content:center}</style>
<style id="wp-block-columns-inline-css">.wp-block-columns{align-items:normal!important;box-sizing:border-box;display:flex;flex-wrap:wrap!important}@media (min-width:782px){.wp-block-columns{flex-wrap:nowrap!important}}.wp-block-columns.are-vertically-aligned-top{align-items:flex-start}.wp-block-columns.are-vertically-aligned-center{align-items:center}.wp-block-columns.are-vertically-aligned-bottom{align-items:flex-end}@media (max-width:781px){.wp-block-columns:not(.is-not-stacked-on-mobile)>.wp-block-column{flex-basis:100%!important}}@media (min-width:782px){.wp-block-columns:not(.is-not-stacked-on-mobile)>.wp-block-column{flex-basis:0;flex-grow:1}.wp-block-columns:not(.is-not-stacked-on-mobile)>.wp-block-column[style*=flex-basis]{flex-grow:0}}.wp-block-columns.is-not-stacked-on-mobile{flex-wrap:nowrap!important}.wp-block-columns.is-not-stacked-on-mobile>.wp-block-column{flex-basis:0;flex-grow:1}.wp-block-columns.is-not-stacked-on-mobile>.wp-block-column[style*=flex-basis]{flex-grow:0}:where(.wp-block-columns){margin-bottom:1.75em}:where(.wp-block-columns.has-background){padding:1.25em 2.375em}.wp-block-column{flex-grow:1;min-width:0;overflow-wrap:break-word;word-break:break-word}.wp-block-column.is-vertically-aligned-top{align-self:flex-start}.wp-block-column.is-vertically-aligned-center{align-self:center}.wp-block-column.is-vertically-aligned-bottom{align-self:flex-end}.wp-block-column.is-vertically-aligned-stretch{align-self:stretch}.wp-block-column.is-vertically-aligned-bottom,.wp-block-column.is-vertically-aligned-center,.wp-block-column.is-vertically-aligned-top{width:100%}</style>
<style id="wp-emoji-styles-inline-css">img.wp-smiley, img.emoji {
display: inline !important;
border: none !important;
box-shadow: none !important;
height: 1em !important;
width: 1em !important;
margin: 0 0.07em !important;
vertical-align: -0.1em !important;
background: none !important;
padding: 0 !important;
}</style>
<style id="wp-block-library-inline-css">:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color--rgb:0,124,186;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-10--rgb:0,107,161;--wp-admin-theme-color-darker-20:#005a87;--wp-admin-theme-color-darker-20--rgb:0,90,135;--wp-admin-border-width-focus:2px;--wp-block-synced-color:#7a00df;--wp-block-synced-color--rgb:122,0,223;--wp-bound-block-color:var(--wp-block-synced-color)}@media (min-resolution:192dpi){:root{--wp-admin-border-width-focus:1.5px}}.wp-element-button{cursor:pointer}:root{--wp--preset--font-size--normal:16px;--wp--preset--font-size--huge:42px}:root .has-very-light-gray-background-color{background-color:#eee}:root .has-very-dark-gray-background-color{background-color:#313131}:root .has-very-light-gray-color{color:#eee}:root .has-very-dark-gray-color{color:#313131}:root .has-vivid-green-cyan-to-vivid-cyan-blue-gradient-background{background:linear-gradient(135deg,#00d084,#0693e3)}:root .has-purple-crush-gradient-background{background:linear-gradient(135deg,#34e2e4,#4721fb 50%,#ab1dfe)}:root .has-hazy-dawn-gradient-background{background:linear-gradient(135deg,#faaca8,#dad0ec)}:root .has-subdued-olive-gradient-background{background:linear-gradient(135deg,#fafae1,#67a671)}:root .has-atomic-cream-gradient-background{background:linear-gradient(135deg,#fdd79a,#004a59)}:root .has-nightshade-gradient-background{background:linear-gradient(135deg,#330968,#31cdcf)}:root .has-midnight-gradient-background{background:linear-gradient(135deg,#020381,#2874fc)}.has-regular-font-size{font-size:1em}.has-larger-font-size{font-size:2.625em}.has-normal-font-size{font-size:var(--wp--preset--font-size--normal)}.has-huge-font-size{font-size:var(--wp--preset--font-size--huge)}.has-text-align-center{text-align:center}.has-text-align-left{text-align:left}.has-text-align-right{text-align:right}#end-resizable-editor-section{display:none}.aligncenter{clear:both}.items-justified-left{justify-content:flex-start}.items-justified-center{justify-content:center}.items-justified-right{justify-content:flex-end}.items-justified-space-between{justify-content:space-between}.screen-reader-text{border:0;clip:rect(1px,1px,1px,1px);clip-path:inset(50%);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;word-wrap:normal!important}.screen-reader-text:focus{background-color:#ddd;clip:auto!important;clip-path:none;color:#444;display:block;font-size:1em;height:auto;left:5px;line-height:normal;padding:15px 23px 14px;text-decoration:none;top:5px;width:auto;z-index:100000}html :where(.has-border-color){border-style:solid}html :where([style*=border-top-color]){border-top-style:solid}html :where([style*=border-right-color]){border-right-style:solid}html :where([style*=border-bottom-color]){border-bottom-style:solid}html :where([style*=border-left-color]){border-left-style:solid}html :where([style*=border-width]){border-style:solid}html :where([style*=border-top-width]){border-top-style:solid}html :where([style*=border-right-width]){border-right-style:solid}html :where([style*=border-bottom-width]){border-bottom-style:solid}html :where([style*=border-left-width]){border-left-style:solid}html :where(img[class*=wp-image-]){height:auto;max-width:100%}:where(figure){margin:0 0 1em}html :where(.is-position-sticky){--wp-admin--admin-bar--position-offset:var(--wp-admin--admin-bar--height,0px)}@media screen and (max-width:600px){html :where(.is-position-sticky){--wp-admin--admin-bar--position-offset:0px}}</style>
<style id="global-styles-inline-css">:root{--wp--preset--aspect-ratio--square: 1;--wp--preset--aspect-ratio--4-3: 4/3;--wp--preset--aspect-ratio--3-4: 3/4;--wp--preset--aspect-ratio--3-2: 3/2;--wp--preset--aspect-ratio--2-3: 2/3;--wp--preset--aspect-ratio--16-9: 16/9;--wp--preset--aspect-ratio--9-16: 9/16;--wp--preset--color--black: #000000;--wp--preset--color--cyan-bluish-gray: #abb8c3;--wp--preset--color--white: #ffffff;--wp--preset--color--pale-pink: #f78da7;--wp--preset--color--vivid-red: #cf2e2e;--wp--preset--color--luminous-vivid-orange: #ff6900;--wp--preset--color--luminous-vivid-amber: #fcb900;--wp--preset--color--light-green-cyan: #7bdcb5;--wp--preset--color--vivid-green-cyan: #00d084;--wp--preset--color--pale-cyan-blue: #8ed1fc;--wp--preset--color--vivid-cyan-blue: #0693e3;--wp--preset--color--vivid-purple: #9b51e0;--wp--preset--color--foreground: #FFF;--wp--preset--color--background: #2C2C2C;--wp--preset--color--primary: #FF502D;--wp--preset--color--secondary: #AAA;--wp--preset--color--tertiary: #555;--wp--preset--color--quaternary: #444;--wp--preset--gradient--vivid-cyan-blue-to-vivid-purple: linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%);--wp--preset--gradient--light-green-cyan-to-vivid-green-cyan: linear-gradient(135deg,rgb(122,220,180) 0%,rgb(0,208,130) 100%);--wp--preset--gradient--luminous-vivid-amber-to-luminous-vivid-orange: linear-gradient(135deg,rgba(252,185,0,1) 0%,rgba(255,105,0,1) 100%);--wp--preset--gradient--luminous-vivid-orange-to-vivid-red: linear-gradient(135deg,rgba(255,105,0,1) 0%,rgb(207,46,46) 100%);--wp--preset--gradient--very-light-gray-to-cyan-bluish-gray: linear-gradient(135deg,rgb(238,238,238) 0%,rgb(169,184,195) 100%);--wp--preset--gradient--cool-to-warm-spectrum: linear-gradient(135deg,rgb(74,234,220) 0%,rgb(151,120,209) 20%,rgb(207,42,186) 40%,rgb(238,44,130) 60%,rgb(251,105,98) 80%,rgb(254,248,76) 100%);--wp--preset--gradient--blush-light-purple: linear-gradient(135deg,rgb(255,206,236) 0%,rgb(152,150,240) 100%);--wp--preset--gradient--blush-bordeaux: linear-gradient(135deg,rgb(254,205,165) 0%,rgb(254,45,45) 50%,rgb(107,0,62) 100%);--wp--preset--gradient--luminous-dusk: linear-gradient(135deg,rgb(255,203,112) 0%,rgb(199,81,192) 50%,rgb(65,88,208) 100%);--wp--preset--gradient--pale-ocean: linear-gradient(135deg,rgb(255,245,203) 0%,rgb(182,227,212) 50%,rgb(51,167,181) 100%);--wp--preset--gradient--electric-grass: linear-gradient(135deg,rgb(202,248,128) 0%,rgb(113,206,126) 100%);--wp--preset--gradient--midnight: linear-gradient(135deg,rgb(2,3,129) 0%,rgb(40,116,252) 100%);--wp--preset--gradient--foreground-to-background: linear-gradient(160deg, var(--wp--preset--color--foreground), var(--wp--preset--color--background));--wp--preset--gradient--foreground-to-primary: linear-gradient(160deg, var(--wp--preset--color--foreground), var(--wp--preset--color--primary));--wp--preset--gradient--foreground-to-secondary: linear-gradient(160deg, var(--wp--preset--color--foreground), var(--wp--preset--color--secondary));--wp--preset--gradient--foreground-to-quaternary: linear-gradient(160deg, var(--wp--preset--color--foreground), var(--wp--preset--color--quaternary));--wp--preset--gradient--foreground-to-tertiary: linear-gradient(160deg, var(--wp--preset--color--foreground), var(--wp--preset--color--tertiary));--wp--preset--gradient--background-to-foreground: linear-gradient(160deg, var(--wp--preset--color--background), var(--wp--preset--color--foreground));--wp--preset--gradient--background-to-primary: linear-gradient(160deg, var(--wp--preset--color--background), var(--wp--preset--color--primary));--wp--preset--gradient--background-to-secondary: linear-gradient(160deg, var(--wp--preset--color--background), var(--wp--preset--color--secondary));--wp--preset--gradient--background-to-quaternary: linear-gradient(160deg, var(--wp--preset--color--background), var(--wp--preset--color--quaternary));--wp--preset--gradient--background-to-tertiary: linear-gradient(160deg, var(--wp--preset--color--background), var(--wp--preset--color--tertiary));--wp--preset--gradient--primary-to-background: linear-gradient(160deg, var(--wp--preset--color--primary), var(--wp--preset--color--background));--wp--preset--gradient--primary-to-foreground: linear-gradient(160deg, var(--wp--preset--color--primary), var(--wp--preset--color--foreground));--wp--preset--gradient--primary-to-secondary: linear-gradient(160deg, var(--wp--preset--color--primary), var(--wp--preset--color--secondary));--wp--preset--gradient--primary-to-quaternary: linear-gradient(160deg, var(--wp--preset--color--primary), var(--wp--preset--color--quaternary));--wp--preset--gradient--primary-to-tertiary: linear-gradient(160deg, var(--wp--preset--color--primary), var(--wp--preset--color--tertiary));--wp--preset--gradient--secondary-to-background: linear-gradient(160deg, var(--wp--preset--color--secondary), var(--wp--preset--color--background));--wp--preset--gradient--secondary-to-foreground: linear-gradient(160deg, var(--wp--preset--color--secondary), var(--wp--preset--color--foreground));--wp--preset--gradient--secondary-to-primary: linear-gradient(160deg, var(--wp--preset--color--secondary), var(--wp--preset--color--primary));--wp--preset--gradient--secondary-to-quaternary: linear-gradient(160deg, var(--wp--preset--color--secondary), var(--wp--preset--color--quaternary));--wp--preset--gradient--secondary-to-tertiary: linear-gradient(160deg, var(--wp--preset--color--secondary), var(--wp--preset--color--tertiary));--wp--preset--gradient--tertiary-to-background: linear-gradient(160deg, var(--wp--preset--color--tertiary), var(--wp--preset--color--background));--wp--preset--gradient--tertiary-to-foreground: linear-gradient(160deg, var(--wp--preset--color--tertiary), var(--wp--preset--color--foreground));--wp--preset--gradient--tertiary-to-primary: linear-gradient(160deg, var(--wp--preset--color--tertiary), var(--wp--preset--color--primary));--wp--preset--gradient--tertiary-to-secondary: linear-gradient(160deg, var(--wp--preset--color--tertiary), var(--wp--preset--color--secondary));--wp--preset--gradient--tertiary-to-quaternary: linear-gradient(160deg, var(--wp--preset--color--tertiary), var(--wp--preset--color--quaternary));--wp--preset--gradient--quaternary-to-background: linear-gradient(160deg, var(--wp--preset--color--quaternary), var(--wp--preset--color--background));--wp--preset--gradient--quaternary-to-foreground: linear-gradient(160deg, var(--wp--preset--color--quaternary), var(--wp--preset--color--foreground));--wp--preset--gradient--quaternary-to-primary: linear-gradient(160deg, var(--wp--preset--color--quaternary), var(--wp--preset--color--primary));--wp--preset--gradient--quaternary-to-secondary: linear-gradient(160deg, var(--wp--preset--color--quaternary), var(--wp--preset--color--secondary));--wp--preset--gradient--quaternary-to-tertiary: linear-gradient(160deg, var(--wp--preset--color--quaternary), var(--wp--preset--color--tertiary));--wp--preset--font-size--small: clamp(16px, 1rem + ((1vw - 3.2px) * 0.147), 18px);--wp--preset--font-size--medium: clamp(17px, 1.063rem + ((1vw - 3.2px) * 0.294), 21px);--wp--preset--font-size--large: clamp(21px, 1.313rem + ((1vw - 3.2px) * 0.221), 24px);--wp--preset--font-size--x-large: clamp(25.014px, 1.563rem + ((1vw - 3.2px) * 1.249), 42px);--wp--preset--font-size--tiny: clamp(14px, 0.875rem + ((1vw - 3.2px) * 0.147), 16px);--wp--preset--font-size--extra-large: clamp(24px, 1.5rem + ((1vw - 3.2px) * 0.588), 32px);--wp--preset--font-size--huge: clamp(36px, 2.25rem + ((1vw - 3.2px) * 2.059), 64px);--wp--preset--font-size--gigantic: clamp(64px, 4rem + ((1vw - 3.2px) * 2.353), 96px);--wp--preset--font-size--heading-1: clamp(36px, 2.25rem + ((1vw - 3.2px) * 2.059), 64px);--wp--preset--font-size--heading-2: clamp(33px, 2.063rem + ((1vw - 3.2px) * 1.691), 56px);--wp--preset--font-size--heading-3: clamp(30px, 1.875rem + ((1vw - 3.2px) * 1.324), 48px);--wp--preset--font-size--heading-4: clamp(27px, 1.688rem + ((1vw - 3.2px) * 0.956), 40px);--wp--preset--font-size--heading-5: clamp(24px, 1.5rem + ((1vw - 3.2px) * 0.588), 32px);--wp--preset--font-size--heading-6: clamp(21px, 1.313rem + ((1vw - 3.2px) * 0.441), 27px);--wp--preset--font-family--default: "Albert Sans", ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;--wp--preset--font-family--system-sans-serif: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;--wp--preset--font-family--system-serif: ui-serif, Georgia, serif;--wp--preset--font-family--system-monospace: ui-monospace, Menlo, Consolas, Monaco, "Liberation Mono", "Lucida Console", monospace;--wp--preset--spacing--20: 12px;--wp--preset--spacing--30: 16px;--wp--preset--spacing--40: 24px;--wp--preset--spacing--50: 32px;--wp--preset--spacing--60: clamp(32px, 7.5vw, 40px);--wp--preset--spacing--70: clamp(48px, 9vw, 64px);--wp--preset--spacing--80: clamp( 64px, 6.666vw, 96px );--wp--preset--spacing--10: 8px;--wp--preset--spacing--body-gutter: clamp( 32px, 5vw, 64px );--wp--preset--shadow--natural: 6px 6px 9px rgba(0, 0, 0, 0.2);--wp--preset--shadow--deep: 12px 12px 50px rgba(0, 0, 0, 0.4);--wp--preset--shadow--sharp: 6px 6px 0px rgba(0, 0, 0, 0.2);--wp--preset--shadow--outlined: 6px 6px 0px -3px rgba(255, 255, 255, 1), 6px 6px rgba(0, 0, 0, 1);--wp--preset--shadow--crisp: 6px 6px 0px rgba(0, 0, 0, 1);--wp--custom--spacing--baseline: 16px;--wp--custom--spacing--small: min(32px, 6.4vw);--wp--custom--spacing--gutter: clamp( calc( 2 * var( --wp--custom--spacing--baseline ) ), 6.666vw, calc( 4 * var( --wp--custom--spacing--baseline ) ) );--wp--custom--spacing--outer: var( --wp--custom--spacing--gutter );--wp--custom--typography--letter-spacing--body: -0.01em;--wp--custom--typography--letter-spacing--heading: -0.02em;--wp--custom--typography--letter-spacing--gigantic: -0.03em;--wp--custom--typography--letter-spacing--uppercase: 0;--wp--custom--typography--line-height--body: 1.5em;--wp--custom--typography--line-height--headings--gigantic: 1.05;--wp--custom--typography--line-height--headings--large: 1.15;--wp--custom--typography--line-height--headings--small: 1.25;}:root { --wp--style--global--content-size: 1300px;--wp--style--global--wide-size: 1680px; }:where(body) { margin: 0; }.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }.has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); }.has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) { padding-right: 0; padding-left: 0; }.has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) > .alignfull { margin-left: 0; margin-right: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: min(32px, 6.4vw); margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: min(32px, 6.4vw); }:root :where(.is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.is-layout-flow) > *{margin-block-start: min(32px, 6.4vw);margin-block-end: 0;}:root :where(.is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.is-layout-constrained) > *{margin-block-start: min(32px, 6.4vw);margin-block-end: 0;}:root :where(.is-layout-flex){gap: min(32px, 6.4vw);}:root :where(.is-layout-grid){gap: min(32px, 6.4vw);}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}body{background-color: var(--wp--preset--color--background);color: var(--wp--preset--color--foreground);font-family: var(--wp--preset--font-family--default);font-size: var(--wp--preset--font-size--medium);font-weight: 500;letter-spacing: var(--wp--custom--typography--letter-spacing--body);line-height: var(--wp--custom--typography--line-height--body);--wp--style--root--padding-top: var(--wp--preset--spacing--body-gutter);--wp--style--root--padding-right: var(--wp--preset--spacing--body-gutter);--wp--style--root--padding-bottom: var(--wp--preset--spacing--body-gutter);--wp--style--root--padding-left: var(--wp--preset--spacing--body-gutter);}a:where(:not(.wp-element-button)){color: inherit;text-decoration: underline;}:root :where(a:where(:not(.wp-element-button)):hover){text-decoration: none;}h1, h2, h3, h4, h5, h6{font-weight: 600;letter-spacing: var(--wp--custom--typography--letter-spacing--gigantic);margin-top: .75em;margin-bottom: 1em;}h1{font-size: var(--wp--preset--font-size--heading-1);line-height: var(--wp--custom--typography--line-height--headings--large);}h2{font-size: var(--wp--preset--font-size--heading-2);line-height: var(--wp--custom--typography--line-height--headings--large);}h3{font-size: var(--wp--preset--font-size--heading-3);line-height: var(--wp--custom--typography--line-height--headings--large);}h4{font-size: var(--wp--preset--font-size--heading-4);line-height: var(--wp--custom--typography--line-height--headings--large);}h5{font-size: var(--wp--preset--font-size--heading-5);line-height: var(--wp--custom--typography--line-height--headings--small);}h6{font-size: var(--wp--preset--font-size--heading-6);line-height: var(--wp--custom--typography--line-height--headings--small);}:root :where(.wp-element-button, .wp-block-button__link){background-color: var(--wp--preset--color--primary);border-radius: 999px;border-width: 0;color: var(--wp--preset--color--background);font-family: inherit;font-size: var(--wp--preset--font-size--small);font-weight: 600;line-height: var(--wp--custom--typography--line-height--headings--large);padding: calc(0.667em + 2px) calc(1.333em + 2px);text-decoration: none;}:root :where(.wp-element-caption, .wp-block-audio figcaption, .wp-block-embed figcaption, .wp-block-gallery figcaption, .wp-block-image figcaption, .wp-block-table figcaption, .wp-block-video figcaption){color: var(--wp--preset--color--secondary);font-size: var(--wp--preset--font-size--small);margin-top: .75em;margin-bottom: 0;}cite{font-size: var(--wp--preset--font-size--medium);font-style: normal;text-transform: none;}.has-black-color{color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-color{color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-color{color: var(--wp--preset--color--white) !important;}.has-pale-pink-color{color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-color{color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-color{color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-color{color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-color{color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-color{color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-color{color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-color{color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-color{color: var(--wp--preset--color--vivid-purple) !important;}.has-foreground-color{color: var(--wp--preset--color--foreground) !important;}.has-background-color{color: var(--wp--preset--color--background) !important;}.has-primary-color{color: var(--wp--preset--color--primary) !important;}.has-secondary-color{color: var(--wp--preset--color--secondary) !important;}.has-tertiary-color{color: var(--wp--preset--color--tertiary) !important;}.has-quaternary-color{color: var(--wp--preset--color--quaternary) !important;}.has-black-background-color{background-color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-background-color{background-color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-background-color{background-color: var(--wp--preset--color--white) !important;}.has-pale-pink-background-color{background-color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-background-color{background-color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-background-color{background-color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-background-color{background-color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-background-color{background-color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-background-color{background-color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-background-color{background-color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-background-color{background-color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-background-color{background-color: var(--wp--preset--color--vivid-purple) !important;}.has-foreground-background-color{background-color: var(--wp--preset--color--foreground) !important;}.has-background-background-color{background-color: var(--wp--preset--color--background) !important;}.has-primary-background-color{background-color: var(--wp--preset--color--primary) !important;}.has-secondary-background-color{background-color: var(--wp--preset--color--secondary) !important;}.has-tertiary-background-color{background-color: var(--wp--preset--color--tertiary) !important;}.has-quaternary-background-color{background-color: var(--wp--preset--color--quaternary) !important;}.has-black-border-color{border-color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-border-color{border-color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-border-color{border-color: var(--wp--preset--color--white) !important;}.has-pale-pink-border-color{border-color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-border-color{border-color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-border-color{border-color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-border-color{border-color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-border-color{border-color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-border-color{border-color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-border-color{border-color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-border-color{border-color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-border-color{border-color: var(--wp--preset--color--vivid-purple) !important;}.has-foreground-border-color{border-color: var(--wp--preset--color--foreground) !important;}.has-background-border-color{border-color: var(--wp--preset--color--background) !important;}.has-primary-border-color{border-color: var(--wp--preset--color--primary) !important;}.has-secondary-border-color{border-color: var(--wp--preset--color--secondary) !important;}.has-tertiary-border-color{border-color: var(--wp--preset--color--tertiary) !important;}.has-quaternary-border-color{border-color: var(--wp--preset--color--quaternary) !important;}.has-vivid-cyan-blue-to-vivid-purple-gradient-background{background: var(--wp--preset--gradient--vivid-cyan-blue-to-vivid-purple) !important;}.has-light-green-cyan-to-vivid-green-cyan-gradient-background{background: var(--wp--preset--gradient--light-green-cyan-to-vivid-green-cyan) !important;}.has-luminous-vivid-amber-to-luminous-vivid-orange-gradient-background{background: var(--wp--preset--gradient--luminous-vivid-amber-to-luminous-vivid-orange) !important;}.has-luminous-vivid-orange-to-vivid-red-gradient-background{background: var(--wp--preset--gradient--luminous-vivid-orange-to-vivid-red) !important;}.has-very-light-gray-to-cyan-bluish-gray-gradient-background{background: var(--wp--preset--gradient--very-light-gray-to-cyan-bluish-gray) !important;}.has-cool-to-warm-spectrum-gradient-background{background: var(--wp--preset--gradient--cool-to-warm-spectrum) !important;}.has-blush-light-purple-gradient-background{background: var(--wp--preset--gradient--blush-light-purple) !important;}.has-blush-bordeaux-gradient-background{background: var(--wp--preset--gradient--blush-bordeaux) !important;}.has-luminous-dusk-gradient-background{background: var(--wp--preset--gradient--luminous-dusk) !important;}.has-pale-ocean-gradient-background{background: var(--wp--preset--gradient--pale-ocean) !important;}.has-electric-grass-gradient-background{background: var(--wp--preset--gradient--electric-grass) !important;}.has-midnight-gradient-background{background: var(--wp--preset--gradient--midnight) !important;}.has-foreground-to-background-gradient-background{background: var(--wp--preset--gradient--foreground-to-background) !important;}.has-foreground-to-primary-gradient-background{background: var(--wp--preset--gradient--foreground-to-primary) !important;}.has-foreground-to-secondary-gradient-background{background: var(--wp--preset--gradient--foreground-to-secondary) !important;}.has-foreground-to-quaternary-gradient-background{background: var(--wp--preset--gradient--foreground-to-quaternary) !important;}.has-foreground-to-tertiary-gradient-background{background: var(--wp--preset--gradient--foreground-to-tertiary) !important;}.has-background-to-foreground-gradient-background{background: var(--wp--preset--gradient--background-to-foreground) !important;}.has-background-to-primary-gradient-background{background: var(--wp--preset--gradient--background-to-primary) !important;}.has-background-to-secondary-gradient-background{background: var(--wp--preset--gradient--background-to-secondary) !important;}.has-background-to-quaternary-gradient-background{background: var(--wp--preset--gradient--background-to-quaternary) !important;}.has-background-to-tertiary-gradient-background{background: var(--wp--preset--gradient--background-to-tertiary) !important;}.has-primary-to-background-gradient-background{background: var(--wp--preset--gradient--primary-to-background) !important;}.has-primary-to-foreground-gradient-background{background: var(--wp--preset--gradient--primary-to-foreground) !important;}.has-primary-to-secondary-gradient-background{background: var(--wp--preset--gradient--primary-to-secondary) !important;}.has-primary-to-quaternary-gradient-background{background: var(--wp--preset--gradient--primary-to-quaternary) !important;}.has-primary-to-tertiary-gradient-background{background: var(--wp--preset--gradient--primary-to-tertiary) !important;}.has-secondary-to-background-gradient-background{background: var(--wp--preset--gradient--secondary-to-background) !important;}.has-secondary-to-foreground-gradient-background{background: var(--wp--preset--gradient--secondary-to-foreground) !important;}.has-secondary-to-primary-gradient-background{background: var(--wp--preset--gradient--secondary-to-primary) !important;}.has-secondary-to-quaternary-gradient-background{background: var(--wp--preset--gradient--secondary-to-quaternary) !important;}.has-secondary-to-tertiary-gradient-background{background: var(--wp--preset--gradient--secondary-to-tertiary) !important;}.has-tertiary-to-background-gradient-background{background: var(--wp--preset--gradient--tertiary-to-background) !important;}.has-tertiary-to-foreground-gradient-background{background: var(--wp--preset--gradient--tertiary-to-foreground) !important;}.has-tertiary-to-primary-gradient-background{background: var(--wp--preset--gradient--tertiary-to-primary) !important;}.has-tertiary-to-secondary-gradient-background{background: var(--wp--preset--gradient--tertiary-to-secondary) !important;}.has-tertiary-to-quaternary-gradient-background{background: var(--wp--preset--gradient--tertiary-to-quaternary) !important;}.has-quaternary-to-background-gradient-background{background: var(--wp--preset--gradient--quaternary-to-background) !important;}.has-quaternary-to-foreground-gradient-background{background: var(--wp--preset--gradient--quaternary-to-foreground) !important;}.has-quaternary-to-primary-gradient-background{background: var(--wp--preset--gradient--quaternary-to-primary) !important;}.has-quaternary-to-secondary-gradient-background{background: var(--wp--preset--gradient--quaternary-to-secondary) !important;}.has-quaternary-to-tertiary-gradient-background{background: var(--wp--preset--gradient--quaternary-to-tertiary) !important;}.has-small-font-size{font-size: var(--wp--preset--font-size--small) !important;}.has-medium-font-size{font-size: var(--wp--preset--font-size--medium) !important;}.has-large-font-size{font-size: var(--wp--preset--font-size--large) !important;}.has-x-large-font-size{font-size: var(--wp--preset--font-size--x-large) !important;}.has-tiny-font-size{font-size: var(--wp--preset--font-size--tiny) !important;}.has-extra-large-font-size{font-size: var(--wp--preset--font-size--extra-large) !important;}.has-huge-font-size{font-size: var(--wp--preset--font-size--huge) !important;}.has-gigantic-font-size{font-size: var(--wp--preset--font-size--gigantic) !important;}.has-heading-1-font-size{font-size: var(--wp--preset--font-size--heading-1) !important;}.has-heading-2-font-size{font-size: var(--wp--preset--font-size--heading-2) !important;}.has-heading-3-font-size{font-size: var(--wp--preset--font-size--heading-3) !important;}.has-heading-4-font-size{font-size: var(--wp--preset--font-size--heading-4) !important;}.has-heading-5-font-size{font-size: var(--wp--preset--font-size--heading-5) !important;}.has-heading-6-font-size{font-size: var(--wp--preset--font-size--heading-6) !important;}.has-default-font-family{font-family: var(--wp--preset--font-family--default) !important;}.has-system-sans-serif-font-family{font-family: var(--wp--preset--font-family--system-sans-serif) !important;}.has-system-serif-font-family{font-family: var(--wp--preset--font-family--system-serif) !important;}.has-system-monospace-font-family{font-family: var(--wp--preset--font-family--system-monospace) !important;}
:root :where(.wp-block-buttons-is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.wp-block-buttons-is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.wp-block-buttons-is-layout-flow) > *{margin-block-start: 1em;margin-block-end: 0;}:root :where(.wp-block-buttons-is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.wp-block-buttons-is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.wp-block-buttons-is-layout-constrained) > *{margin-block-start: 1em;margin-block-end: 0;}:root :where(.wp-block-buttons-is-layout-flex){gap: 1em;}:root :where(.wp-block-buttons-is-layout-grid){gap: 1em;}
:root :where(.wp-block-image img, .wp-block-image .wp-block-image__crop-area, .wp-block-image .components-placeholder){border-radius: 8px;}
:root :where(.wp-block-navigation a:where(:not(.wp-element-button))){text-decoration: none;}
:root :where(.wp-block-navigation a:where(:not(.wp-element-button)):hover){text-decoration: underline;}
:root :where(p){line-height: var(--wp--custom--typography--line-height--body);}
:root :where(.wp-block-post-date a:where(:not(.wp-element-button))){text-decoration: none;}
:root :where(.wp-block-post-date a:where(:not(.wp-element-button)):hover){text-decoration: underline;}
:root :where(.wp-block-post-title a:where(:not(.wp-element-button))){text-decoration: none;}
:root :where(.wp-block-post-title a:where(:not(.wp-element-button)):hover){text-decoration: underline;}
:root :where(.wp-block-query-pagination a:where(:not(.wp-element-button))){text-decoration: none;}
:root :where(.wp-block-query-pagination a:where(:not(.wp-element-button)):hover){text-decoration: underline;}
:root :where(.wp-block-site-title){font-family: inherit;font-style: inherit;font-weight: 600;margin-top: 0px;margin-bottom: 0px;text-transform: inherit;}
:root :where(.wp-block-site-title a:where(:not(.wp-element-button))){text-decoration: none;}
:root :where(.wp-block-site-title a:where(:not(.wp-element-button)):hover){text-decoration: underline;}
:root :where(.wp-block-social-links-is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.wp-block-social-links-is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.wp-block-social-links-is-layout-flow) > *{margin-block-start: .66em;margin-block-end: 0;}:root :where(.wp-block-social-links-is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.wp-block-social-links-is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.wp-block-social-links-is-layout-constrained) > *{margin-block-start: .66em;margin-block-end: 0;}:root :where(.wp-block-social-links-is-layout-flex){gap: .66em;}:root :where(.wp-block-social-links-is-layout-grid){gap: .66em;}
:root :where(.wp-block-code){background-color: var(--wp--preset--color--background);border-top-color: var(--wp--preset--color--white);border-top-width: 1px;border-top-style: solid;border-right-color: var(--wp--preset--color--white);border-right-width: 1px;border-right-style: solid;border-bottom-color: var(--wp--preset--color--white);border-bottom-width: 1px;border-bottom-style: solid;border-left-color: var(--wp--preset--color--white);border-left-width: 1px;border-left-style: solid;color: var(--wp--preset--color--light-green-cyan);font-family: var(--wp--preset--font-family--system-monospace);font-style: normal;font-weight: 500;padding-top: var(--wp--preset--spacing--20);padding-right: var(--wp--preset--spacing--20);padding-bottom: var(--wp--preset--spacing--20);padding-left: var(--wp--preset--spacing--20);}
:root :where(.wp-block-code a:where(:not(.wp-element-button))){color: var(--wp--preset--color--light-green-cyan);}
pre.wp-block-code {
background: #333333;
color: #4e9a06;
}</style>
<style id="core-block-supports-inline-css">.wp-container-core-navigation-is-layout-1{flex-direction:column;align-items:flex-start;}.wp-container-core-group-is-layout-2{flex-wrap:nowrap;gap:2em;justify-content:space-between;}.wp-container-core-navigation-is-layout-2{gap:0.25em;flex-direction:column;align-items:flex-start;}.wp-container-core-social-links-is-layout-1{gap:1em 1em;justify-content:flex-start;}.wp-container-core-group-is-layout-4{gap:2em;flex-direction:column;align-items:flex-start;}.wp-container-core-group-is-layout-5 > *{margin-block-start:0;margin-block-end:0;}.wp-container-core-group-is-layout-5 > * + *{margin-block-start:.25em;margin-block-end:0;}.wp-container-core-group-is-layout-6{flex-wrap:nowrap;gap:0.25em;}.wp-container-core-group-is-layout-7{flex-wrap:nowrap;gap:0.5em;flex-direction:column;align-items:flex-start;}.wp-elements-971944f10a9f543a8259fe65b5376161 a:where(:not(.wp-element-button)){color:var(--wp--preset--color--primary);}.wp-container-core-query-pagination-is-layout-1{justify-content:space-between;}.wp-container-core-group-is-layout-11 > *{margin-block-start:0;margin-block-end:0;}.wp-container-core-group-is-layout-11 > * + *{margin-block-start:2em;margin-block-end:0;}.wp-container-core-group-is-layout-12{gap:0.5em;justify-content:flex-start;}.wp-container-core-group-is-layout-13{gap:1.5em;}.wp-container-core-group-is-layout-14{gap:1em;justify-content:space-between;}.wp-container-core-column-is-layout-2 > *{margin-block-start:0;margin-block-end:0;}.wp-container-core-column-is-layout-2 > * + *{margin-block-start:var(--wp--preset--spacing--70);margin-block-end:0;}.wp-container-core-columns-is-layout-1{flex-wrap:nowrap;}</style>
<style id="wp-block-template-skip-link-inline-css">.skip-link.screen-reader-text {
border: 0;
clip: rect(1px,1px,1px,1px);
clip-path: inset(50%);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute !important;
width: 1px;
word-wrap: normal !important;
}
.skip-link.screen-reader-text:focus {
background-color: #eee;
clip: auto !important;
clip-path: none;
color: #444;
display: block;
font-size: 1em;
height: auto;
left: 5px;
line-height: normal;
padding: 15px 23px 14px;
text-decoration: none;
top: 5px;
width: auto;
z-index: 100000;
}</style>
<link rel="stylesheet" id="bjork-styles-css" href="/wp-content/themes/bjork/style.css?ver=0.2.3" media="all">
<link rel="https://api.w.org/" href="/wp-json/">
<link rel="alternate" title="JSON" type="application/json" href="/wp-json/wp/v2/posts/539">
<link rel="EditURI" type="application/rsd+xml" title="RSD" href="/xmlrpc.php?rsd">
<meta name="generator" content="WordPress 6.6.2">
<link rel="canonical" href="/roll-your-own-authenticator-app-with-keystonejs-and-react-pt-2/">
<link rel="shortlink" href="/?p=539">
<link rel="alternate" title="oEmbed (JSON)" type="application/json+oembed" href="/wp-json/oembed/1.0/embed?url=https%3A%2F%2F%2Froll-your-own-authenticator-app-with-keystonejs-and-react-pt-2%2F">
<link rel="alternate" title="oEmbed (XML)" type="text/xml+oembed" href="/wp-json/oembed/1.0/embed?url=https%3A%2F%2F%2Froll-your-own-authenticator-app-with-keystonejs-and-react-pt-2%2F#038;format=xml">
<script id="wp-load-polyfill-importmap">
( HTMLScriptElement.supports && HTMLScriptElement.supports("importmap") ) || document.write( '<script src="/wp-includes/js/dist/vendor/wp-polyfill-importmap.min.js?ver=1.8.2"><\/scr' + 'ipt>' );
</script>
<script type="importmap" id="wp-importmap">{"imports":{"@wordpress\/interactivity":"\/wp-includes\/js\/dist\/interactivity.min.js?ver=6.6.2"}}</script>
<script type="module" src="/wp-includes/blocks/navigation/view.min.js?ver=6.6.2" id="@wordpress/block-library/navigation-js-module"></script>
<link rel="modulepreload" href="/wp-includes/js/dist/interactivity.min.js?ver=6.6.2" id="@wordpress/interactivity-js-modulepreload">
<style id="wp-fonts-local">@font-face{font-family:"Albert Sans";font-style:normal;font-weight:100 900;font-display:fallback;src:url('/wp-content/themes/bjork/assets/fonts/AlbertSans-VariableFont_wght.woff2') format('woff2');font-stretch:normal;}
@font-face{font-family:"Albert Sans";font-style:italic;font-weight:100 900;font-display:fallback;src:url('/wp-content/themes/bjork/assets/fonts/AlbertSans-Italic-VariableFont_wght.woff2') format('woff2');font-stretch:normal;}</style>
<link rel="icon" href="/wp-content/uploads/2024/03/cropped-cropped-avatar-32x32.png" sizes="32x32">
<link rel="icon" href="/wp-content/uploads/2024/03/cropped-cropped-avatar-192x192.png" sizes="192x192">
<link rel="apple-touch-icon" href="/wp-content/uploads/2024/03/cropped-cropped-avatar-180x180.png">
<meta name="msapplication-TileImage" content="/wp-content/uploads/2024/03/cropped-cropped-avatar-270x270.png">
</head>
<body class="post-template-default single single-post postid-539 single-format-standard wp-custom-logo wp-embed-responsive">
<div class="wp-site-blocks">
<header class="site-header wp-block-template-part">
<div class="wp-block-group alignfull has-global-padding is-layout-constrained wp-block-group-is-layout-constrained" style="margin-top:0;margin-bottom:0;padding-bottom:var(--wp--preset--spacing--70)">
<div class="wp-block-group alignwide is-content-justification-space-between is-nowrap is-layout-flex wp-container-core-group-is-layout-2 wp-block-group-is-layout-flex">
<div class="wp-block-group has-global-padding is-layout-constrained wp-block-group-is-layout-constrained"><h1 style="font-style:normal;" class="wp-block-site-title has-heading-5-font-size"><a href="/" target="_self" rel="home">hackanooga</a></h1></div>
<nav class="has-large-font-size is-responsive items-justified-left is-vertical wp-block-navigation has-large-font-size is-content-justification-left is-layout-flex wp-container-core-navigation-is-layout-1 wp-block-navigation-is-layout-flex" aria-label="Navigation 5" data-wp-interactive="core/navigation" data-wp-context='{"overlayOpenedBy":{"click":false,"hover":false,"focus":false},"type":"overlay","roleAttribute":"","ariaLabel":"Menu"}'><button aria-haspopup="dialog" aria-label="Open menu" class="wp-block-navigation__responsive-container-open always-shown" data-wp-on-async--click="actions.openMenuOnClick" data-wp-on--keydown="actions.handleMenuKeydown"><svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="4" y="7.5" width="16" height="1.5"></rect><rect x="4" y="15" width="16" height="1.5"></rect></svg></button>
<div class="wp-block-navigation__responsive-container hidden-by-default " id="modal-1" data-wp-class--has-modal-open="state.isMenuOpen" data-wp-class--is-menu-open="state.isMenuOpen" data-wp-watch="callbacks.initMenu" data-wp-on--keydown="actions.handleMenuKeydown" data-wp-on-async--focusout="actions.handleMenuFocusout" tabindex="-1">
<div class="wp-block-navigation__responsive-close" tabindex="-1">
<div class="wp-block-navigation__responsive-dialog" data-wp-bind--aria-modal="state.ariaModal" data-wp-bind--aria-label="state.ariaLabel" data-wp-bind--role="state.roleAttribute">
<button aria-label="Close menu" class="wp-block-navigation__responsive-container-close" data-wp-on-async--click="actions.closeMenuOnClick"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="M13 11.8l6.1-6.3-1-1-6.1 6.2-6.1-6.2-1 1 6.1 6.3-6.5 6.7 1 1 6.5-6.6 6.5 6.6 1-1z"></path></svg></button>
<div class="wp-block-navigation__responsive-container-content" data-wp-watch="callbacks.focusFirstElement" id="modal-1-content">
<ul class="wp-block-navigation__container has-large-font-size is-responsive items-justified-left is-vertical wp-block-navigation has-large-font-size">
<li class="has-large-font-size wp-block-navigation-item wp-block-navigation-link"><a class="wp-block-navigation-item__content" href="/blog/"><span class="wp-block-navigation-item__label">Blog</span></a></li>
<li class="has-large-font-size wp-block-navigation-item wp-block-navigation-link"><a class="wp-block-navigation-item__content" href="/wp-content/uploads/2024/03/mikeconrad-devops.pdf"><span class="wp-block-navigation-item__label">Resume</span></a></li>
<li class="has-large-font-size wp-block-navigation-item wp-block-navigation-link"><a class="wp-block-navigation-item__content" href="/portfolio/"><span class="wp-block-navigation-item__label">Portfolio</span></a></li>
<li class="has-large-font-size wp-block-navigation-item wp-block-navigation-link"><a class="wp-block-navigation-item__content" href="https://linkedin.com/in/mikeenxo"><span class="wp-block-navigation-item__label">Connect</span></a></li>
<li class="has-large-font-size wp-block-navigation-item wp-block-navigation-link"><a class="wp-block-navigation-item__content" href="https://meetup.com/chadevs"><span class="wp-block-navigation-item__label">Chadev</span></a></li>
<li class="has-large-font-size wp-block-navigation-item wp-block-navigation-link"><a class="wp-block-navigation-item__content" href="/"><span class="wp-block-navigation-item__label">Home</span></a></li>
</ul>
</div>
</div>
</div>
</div></nav>
</div>
</div>
</header>
<div class="wp-block-group has-global-padding is-layout-constrained wp-block-group-is-layout-constrained">
<div class="wp-block-columns alignwide is-layout-flex wp-container-core-columns-is-layout-1 wp-block-columns-is-layout-flex" style="margin-bottom:0px">
<div class="wp-block-column site-sidebar-col is-layout-flow wp-block-column-is-layout-flow" style="flex-basis:23%"><aside class="site-sidebar wp-block-template-part">
<div class="wp-block-group is-vertical is-layout-flex wp-container-core-group-is-layout-4 wp-block-group-is-layout-flex" style="margin-top:0px;margin-bottom:0px">
<h1 style="font-style:normal;" class="wp-block-site-title has-heading-5-font-size"><a href="/" target="_self" rel="home">hackanooga</a></h1>
<nav class="has-large-font-size is-responsive is-vertical wp-block-navigation has-large-font-size is-layout-flex wp-container-core-navigation-is-layout-2 wp-block-navigation-is-layout-flex" aria-label="Navigation 4" data-wp-interactive="core/navigation" data-wp-context='{"overlayOpenedBy":{"click":false,"hover":false,"focus":false},"type":"overlay","roleAttribute":"","ariaLabel":"Menu"}'><button aria-haspopup="dialog" aria-label="Open menu" class="wp-block-navigation__responsive-container-open " data-wp-on-async--click="actions.openMenuOnClick" data-wp-on--keydown="actions.handleMenuKeydown"><svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="4" y="7.5" width="16" height="1.5"></rect><rect x="4" y="15" width="16" height="1.5"></rect></svg></button>
<div class="wp-block-navigation__responsive-container " id="modal-2" data-wp-class--has-modal-open="state.isMenuOpen" data-wp-class--is-menu-open="state.isMenuOpen" data-wp-watch="callbacks.initMenu" data-wp-on--keydown="actions.handleMenuKeydown" data-wp-on-async--focusout="actions.handleMenuFocusout" tabindex="-1">
<div class="wp-block-navigation__responsive-close" tabindex="-1">
<div class="wp-block-navigation__responsive-dialog" data-wp-bind--aria-modal="state.ariaModal" data-wp-bind--aria-label="state.ariaLabel" data-wp-bind--role="state.roleAttribute">
<button aria-label="Close menu" class="wp-block-navigation__responsive-container-close" data-wp-on-async--click="actions.closeMenuOnClick"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="M13 11.8l6.1-6.3-1-1-6.1 6.2-6.1-6.2-1 1 6.1 6.3-6.5 6.7 1 1 6.5-6.6 6.5 6.6 1-1z"></path></svg></button>
<div class="wp-block-navigation__responsive-container-content" data-wp-watch="callbacks.focusFirstElement" id="modal-2-content">
<ul class="wp-block-navigation__container has-large-font-size is-responsive is-vertical wp-block-navigation has-large-font-size">
<li class="has-large-font-size wp-block-navigation-item wp-block-navigation-link"><a class="wp-block-navigation-item__content" href="/"><span class="wp-block-navigation-item__label">Intro</span></a></li>
<li class="has-large-font-size wp-block-navigation-item wp-block-navigation-link"><a class="wp-block-navigation-item__content" href="/blog/"><span class="wp-block-navigation-item__label">Blog</span></a></li>
<li class="has-large-font-size wp-block-navigation-item wp-block-navigation-link"><a class="wp-block-navigation-item__content" href="/wp-content/uploads/2024/03/mikeconrad-devops.pdf"><span class="wp-block-navigation-item__label">Resume</span></a></li>
<li class="has-large-font-size wp-block-navigation-item wp-block-navigation-link"><a class="wp-block-navigation-item__content" href="/portfolio/"><span class="wp-block-navigation-item__label">Portfolio</span></a></li>
</ul>
</div>
</div>
</div>
</div></nav>
<ul class="wp-block-social-links has-icon-color is-style-logos-only is-content-justification-left is-layout-flex wp-container-core-social-links-is-layout-1 wp-block-social-links-is-layout-flex" style="margin-top:0px;margin-bottom:0px">
<li style="color: inherit; " class="wp-social-link wp-social-link-linkedin has-foreground-color wp-block-social-link"><a href="https://linkedin.com/in/mikeenxo" class="wp-block-social-link-anchor"><svg width="24" height="24" viewbox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false"><path d="M19.7,3H4.3C3.582,3,3,3.582,3,4.3v15.4C3,20.418,3.582,21,4.3,21h15.4c0.718,0,1.3-0.582,1.3-1.3V4.3 C21,3.582,20.418,3,19.7,3z M8.339,18.338H5.667v-8.59h2.672V18.338z M7.004,8.574c-0.857,0-1.549-0.694-1.549-1.548 c0-0.855,0.691-1.548,1.549-1.548c0.854,0,1.547,0.694,1.547,1.548C8.551,7.881,7.858,8.574,7.004,8.574z M18.339,18.338h-2.669 v-4.177c0-0.996-0.017-2.278-1.387-2.278c-1.389,0-1.601,1.086-1.601,2.206v4.249h-2.667v-8.59h2.559v1.174h0.037 c0.356-0.675,1.227-1.387,2.526-1.387c2.703,0,3.203,1.779,3.203,4.092V18.338z"></path></svg><span class="wp-block-social-link-label screen-reader-text">LinkedIn</span></a></li>
<li style="color: inherit; " class="wp-social-link wp-social-link-github has-foreground-color wp-block-social-link"><a href="https://github.com/enxoco" class="wp-block-social-link-anchor"><svg width="24" height="24" viewbox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false"><path d="M12,2C6.477,2,2,6.477,2,12c0,4.419,2.865,8.166,6.839,9.489c0.5,0.09,0.682-0.218,0.682-0.484 c0-0.236-0.009-0.866-0.014-1.699c-2.782,0.602-3.369-1.34-3.369-1.34c-0.455-1.157-1.11-1.465-1.11-1.465 c-0.909-0.62,0.069-0.608,0.069-0.608c1.004,0.071,1.532,1.03,1.532,1.03c0.891,1.529,2.341,1.089,2.91,0.833 c0.091-0.647,0.349-1.086,0.635-1.337c-2.22-0.251-4.555-1.111-4.555-4.943c0-1.091,0.39-1.984,1.03-2.682 C6.546,8.54,6.202,7.524,6.746,6.148c0,0,0.84-0.269,2.75,1.025C10.295,6.95,11.15,6.84,12,6.836 c0.85,0.004,1.705,0.114,2.504,0.336c1.909-1.294,2.748-1.025,2.748-1.025c0.546,1.376,0.202,2.394,0.1,2.646 c0.64,0.699,1.026,1.591,1.026,2.682c0,3.841-2.337,4.687-4.565,4.935c0.359,0.307,0.679,0.917,0.679,1.852 c0,1.335-0.012,2.415-0.012,2.741c0,0.269,0.18,0.579,0.688,0.481C19.138,20.161,22,16.416,22,12C22,6.477,17.523,2,12,2z"></path></svg><span class="wp-block-social-link-label screen-reader-text">GitHub</span></a></li>
</ul>
</div>
</aside></div>
<div class="wp-block-column is-layout-flow wp-container-core-column-is-layout-2 wp-block-column-is-layout-flow" style="flex-basis:77%">
<div class="wp-block-group has-global-padding is-layout-constrained wp-container-core-group-is-layout-5 wp-block-group-is-layout-constrained">
<div style="font-style:normal;font-weight:600;" class="wp-block-post-date has-text-color has-primary-color has-heading-5-font-size"><time datetime="2024-01-10T20:41:00-05:00">January 10, 2024</time></div>
<h1 class="has-text-align-left wp-block-post-title">Roll your own authenticator app with KeystoneJS and React &#8211; pt 2</h1>
</div>
<div class="entry-content wp-block-post-content has-global-padding is-layout-constrained wp-block-post-content-is-layout-constrained">
<p>In part 1 of this series we built out a basic backend using KeystoneJS. In this part we will go ahead and start a new React frontend that will interact with our backend. We will be using Vite. Let&#8217;s get started. Make sure you are in the <code>authenticator</code> folder and run the following:</p>
<pre class="wp-block-code"><code>$ yarn create vite@latest
yarn create v1.22.21
&#91;1/4] Resolving packages...
&#91;2/4] Fetching packages...
&#91;3/4] Linking dependencies...
&#91;4/4] Building fresh packages...
success Installed "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="ed8e9f888c9988c09b849988add8c3dfc3df">[email&#160;protected]</a>" with binaries:
- create-vite
- cva
✔ Project name: … frontend
✔ Select a framework: React
✔ Select a variant: TypeScript
Scaffolding project in /home/mikeconrad/projects/authenticator/frontend...
Done. Now run:
cd frontend
yarn
yarn dev
Done in 10.20s.</code></pre>
<p>Let&#8217;s go ahead and go into our frontend directory and get started:</p>
<pre class="wp-block-code"><code>$ cd frontend
$ yarn
yarn install v1.22.21
info No lockfile found.
&#91;1/4] Resolving packages...
&#91;2/4] Fetching packages...
&#91;3/4] Linking dependencies...
&#91;4/4] Building fresh packages...
success Saved lockfile.
Done in 10.21s.
$ yarn dev
yarn run v1.22.21
$ vite
Port 5173 is in use, trying another one...
VITE v5.1.6 ready in 218 ms
➜ Local: http://localhost:5174/
➜ Network: use --host to expose
➜ press h + enter to show help
</code></pre>
<p>Next go ahead and open the project up in your IDE of choice. I prefer <a href="https://vscodium.com/">VSCodium</a>:</p>
<pre class="wp-block-code"><code>codium frontend</code></pre>
<p>Go ahead and open up <code>src/App.tsx</code> and remove all the boilerplate so it looks like this:</p>
<pre class="wp-block-code"><code>import './App.css'
function App() {
return (
&lt;&gt;
&lt;/&gt;
)
}
export default App</code></pre>
<p>Let&#8217;s start by building a card component that will display an individual token. Our goal is something that looks like this:</p>
<figure class="wp-block-image size-full"><img fetchpriority="high" decoding="async" width="928" height="320" src="/wp-content/uploads/2024/03/ente-auth.webp" alt="" class="wp-image-540" srcset="/wp-content/uploads/2024/03/ente-auth.webp 928w, /wp-content/uploads/2024/03/ente-auth-300x103.webp 300w, /wp-content/uploads/2024/03/ente-auth-768x265.webp 768w" sizes="(max-width: 928px) 100vw, 928px"></figure>
<p>We will start by creating a Components folder with a Card component:</p>
<pre class="wp-block-code"><code>$ mkdir src/Components
$ touch src/Components/Card.tsx</code></pre>
<p>Let&#8217;s go ahead and make a couple updates, we will create this simple card component, add some dummy tokens and some basic styling.</p>
<pre class="wp-block-code"><code># src/App.tsx
import './App.css'
import Card from './Components/Card';
export interface IToken {
account: string;
issuer: string;
token: string;
}
function App() {
const tokens: IToken&#91;] = &#91;
{
account: '<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="a9ccc7d1c6cac6e9cec0ddc1dccb87cac6c4">[email&#160;protected]</a>',
issuer: 'Github',
token: 'AJFDLDAJKFK'
},
{
account: '<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="492420222c2a26273b282d092c31282439252c672a2624">[email&#160;protected]</a>',
issuer: 'Example.com',
token: 'KAJLFDJLKAFD'
}
]
return (
&lt;&gt;
&lt;div className='cardWrapper'&gt;
{tokens.map(token =&gt; &lt;Card token={token} /&gt;)}
&lt;/div&gt;
&lt;/&gt;
)
}
export default App
</code></pre>
<pre class="wp-block-code"><code># src/Components/Card.tsx
import { IToken } from "../App"
function Card({ token }: { token: IToken }) {
return (
&lt;&gt;
&lt;div className='card'&gt;
&lt;span&gt;{token.issuer}&lt;/span&gt;
&lt;span&gt;{token.account}&lt;/span&gt;
&lt;span&gt;{token.token}&lt;/span&gt;
&lt;/div&gt;
&lt;/&gt;
)
}
export default Card</code></pre>
<pre class="wp-block-code"><code># src/index.css
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
background-color: #2c2c2c;
}
.cardWrapper {
display: flex;
}
.card {
padding: 2em;
min-width: 250px;
border: 1px solid;
margin: 10px;
background-color: #333333;
display: flex;
flex-direction: column;
align-items: baseline;
}</code></pre>
<p>Now you should have something that looks like this:</p>
<figure class="wp-block-image size-full"><img decoding="async" width="928" height="320" src="/wp-content/uploads/2024/03/initial-token-frontend.webp" alt="" class="wp-image-541" srcset="/wp-content/uploads/2024/03/initial-token-frontend.webp 928w, /wp-content/uploads/2024/03/initial-token-frontend-300x103.webp 300w, /wp-content/uploads/2024/03/initial-token-frontend-768x265.webp 768w" sizes="(max-width: 928px) 100vw, 928px"></figure>
<p>Alright, we have some of the boring stuff out of the way, now let&#8217;s start making some magic. If you aren&#8217;t familiar with how TOTP tokens work, basically there is an Algorithm that generates them. I would encourage you to read the <a href="https://datatracker.ietf.org/doc/html/rfc6238">RFC</a> for a detailed explanation. Basically it is an algorithm that generates a one time password using the current time as a source of uniqueness along with the secret key.</p>
<p>If we really wanted to we could implement this algorithm ourselves but thankfully there are some really simple libraries that do it for us. For our project we will be using one called <code><a href="https://github.com/bellstrand/totp-generator">totp-generator</a></code>. Let&#8217;s go ahead and install it and check it out:</p>
<pre class="wp-block-code"><code>$ yarn add totp-generator</code></pre>
<p>Now let&#8217;s add it to our card component and see what happens. Using it is really simple. We just need to import it, instantiate a new <code>TokenGenerator</code> and pass it our Secret key:</p>
<pre class="wp-block-code"><code># src/Components/card.tsx
import { TOTP } from 'totp-generator';
---
function Card({ token }: { token: IToken }) {
const { otp, expires } = TOTP.generate(token.token)
return (
&lt;>
&lt;div className='card'>
&lt;span>{token.issuer}&lt;/span>
&lt;span>{token.account}&lt;/span>
&lt;span>{otp} - {expires}&lt;/span>
&lt;/div>
&lt;/>
)
}</code></pre>
<p>Now save and go back to your browser and you should see that our secret keys are now being displayed as tokens:</p>
<figure class="wp-block-image size-full"><img decoding="async" width="928" height="320" src="/wp-content/uploads/2024/03/token-display-1.png" alt="" class="wp-image-542" srcset="/wp-content/uploads/2024/03/token-display-1.png 928w, /wp-content/uploads/2024/03/token-display-1-300x103.png 300w, /wp-content/uploads/2024/03/token-display-1-768x265.png 768w" sizes="(max-width: 928px) 100vw, 928px"></figure>
<p>That is pretty cool, the only problem is you need to refresh the page to refresh the token. We will take care of that in part 3 of this series as well as handling fetching tokens from our backend.</p>
</div>
<div class="wp-block-group has-global-padding is-layout-constrained wp-block-group-is-layout-constrained">
<div class="wp-block-group has-primary-color has-text-color has-secondary-color has-small-font-size is-vertical is-content-justification-left is-nowrap is-layout-flex wp-container-core-group-is-layout-7 wp-block-group-is-layout-flex">
<div class="wp-block-group is-nowrap is-layout-flex wp-container-core-group-is-layout-6 wp-block-group-is-layout-flex">
<p>In</p>
<div class="taxonomy-category wp-block-post-terms"><a href="/category/software-engineering/" rel="tag">Software Engineering</a></div>
</div>
<div class="taxonomy-post_tag wp-block-post-terms">
<a href="/tag/blog-post/" rel="tag">Blog Post</a><span class="wp-block-post-terms__separator">, </span><a href="/tag/graphql/" rel="tag">GraphQL</a><span class="wp-block-post-terms__separator">, </span><a href="/tag/keystonejs/" rel="tag">KeystoneJS</a><span class="wp-block-post-terms__separator">, </span><a href="/tag/nodejs/" rel="tag">NodeJS</a><span class="wp-block-post-terms__separator">, </span><a href="/tag/prisma/" rel="tag">Prisma</a><span class="wp-block-post-terms__separator">, </span><a href="/tag/typescript/" rel="tag">TypeScript</a>
</div>
</div>
</div>
<div class="comments wp-block-template-part">
<div class="wp-block-group has-global-padding is-layout-constrained wp-block-group-is-layout-constrained">
<div class="wp-block-comments-query-loop">
<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow" style="margin-top:64px;margin-bottom:64px">
</div>
</div>
</div>
</div>
<div class="wp-block-group is-layout-flow wp-container-core-group-is-layout-11 wp-block-group-is-layout-flow" style="padding-top:var(--wp--preset--spacing--70)">
<hr class="wp-block-separator has-text-color has-tertiary-color has-alpha-channel-opacity has-tertiary-background-color has-background is-style-bjork-angled-separator-wide">
<nav class="has-link-color alignwide alignwide wp-elements-971944f10a9f543a8259fe65b5376161 wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-container-core-query-pagination-is-layout-1 wp-block-query-pagination-is-layout-flex" aria-label="Pagination">
<div style="font-style:normal;font-weight:600;" class="post-navigation-link-previous wp-block-post-navigation-link"><a href="/roll-your-own-authenticator-app-with-keystonejs-and-react/" rel="prev">Roll your own authenticator app with KeystoneJS and React</a></div>
<div style="font-style:normal;font-weight:600;" class="post-navigation-link-next wp-block-post-navigation-link"><a href="/roll-your-own-authenticator-app-with-keystonejs-and-react-pt-3/" rel="next">Roll your own authenticator app with KeystoneJS and React &#8211; pt 3</a></div>
</nav>
<hr class="wp-block-separator has-text-color has-tertiary-color has-alpha-channel-opacity has-tertiary-background-color has-background is-style-bjork-angled-separator-wide">
</div>
<footer class="site-footer wp-block-template-part">
<div class="wp-block-group has-small-font-size is-content-justification-space-between is-layout-flex wp-container-core-group-is-layout-14 wp-block-group-is-layout-flex" style="padding-top:var(--wp--preset--spacing--70)">
<div class="wp-block-group is-layout-flex wp-container-core-group-is-layout-13 wp-block-group-is-layout-flex">
<div class="wp-block-group is-content-justification-left is-layout-flex wp-container-core-group-is-layout-12 wp-block-group-is-layout-flex" style="font-weight:700/">
<p>© 2023</p>
<p style="font-weight:700;" class="wp-block-site-title"><a href="/" target="_self" rel="home">hackanooga</a></p>
</div>
<p class="theme-credit has-secondary-color has-text-color">Theme by <a href="https://andersnoren.se/">Anders Norén</a></p>
</div>
<p>Powered by <a href="https://wordpress.org/">WordPress</a></p>
</div>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script src="https://openpanel.dev/op.js" defer async></script>
<script src="/wp-content/themes/bjork/assets/js/openpanel.js" defer async></script></footer>
</div>
</div>
</div>
</div>
<script src="/wp-includes/js/comment-reply.min.js?ver=6.6.2" id="comment-reply-js" async data-wp-strategy="async"></script>
<script id="wp-block-template-skip-link-js-after">( function() {
var skipLinkTarget = document.querySelector( 'main' ),
sibling,
skipLinkTargetID,
skipLink;
// Early exit if a skip-link target can't be located.
if ( ! skipLinkTarget ) {
return;
}
/*
* Get the site wrapper.
* The skip-link will be injected in the beginning of it.
*/
sibling = document.querySelector( '.wp-site-blocks' );
// Early exit if the root element was not found.
if ( ! sibling ) {
return;
}
// Get the skip-link target's ID, and generate one if it doesn't exist.
skipLinkTargetID = skipLinkTarget.id;
if ( ! skipLinkTargetID ) {
skipLinkTargetID = 'wp--skip-link--target';
skipLinkTarget.id = skipLinkTargetID;
}
// Create the skip link.
skipLink = document.createElement( 'a' );
skipLink.classList.add( 'skip-link', 'screen-reader-text' );
skipLink.href = '#' + skipLinkTargetID;
skipLink.innerHTML = 'Skip to content';
// Inject the skip link.
sibling.parentElement.insertBefore( skipLink, sibling );
}() );</script>
<script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'8d2109e559be0d06',t:'MTcyODg0MDQyOS4wMDAwMDA='};var a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script>
</body>
</html>