feat: add login otp pages (#9)

Co-authored-by: Anthony Lukin <anthony@lukin.dev>
This commit is contained in:
Ron Mercado 2022-04-06 03:30:00 +07:00
parent 259f10aae6
commit a3f1c1d1d1
17 changed files with 678 additions and 463 deletions

View file

@ -7,7 +7,9 @@ Keywind is a component-based Keycloak Login Theme built with [Tailwind CSS](http
### Styled Pages
- Login
- Login Config TOTP
- Login IDP Link Confirm
- Login OTP
- Login Reset Password
- Login Update Password
- Login Update Profile
@ -77,9 +79,6 @@ You can inherit Keywind components in your own theme. For example, to resize the
When you're ready to deploy your own theme, run the build command to generate a static production build.
```bash
npm install
npm run build
# or
yarn install
yarn build
pnpm install
pnpm build
```

View file

@ -5,16 +5,16 @@
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"alpinejs": "^3.7.1"
"alpinejs": "^3.9.5"
},
"devDependencies": {
"@snowpack/plugin-postcss": "^1.4.3",
"@tailwindcss/forms": "^0.4.0",
"@types/tailwindcss": "^3.0.0",
"autoprefixer": "^10.4.1",
"cssnano": "^5.0.14",
"postcss": "^8.4.5",
"@tailwindcss/forms": "^0.5.0",
"@types/tailwindcss": "^3.0.10",
"autoprefixer": "^10.4.4",
"cssnano": "^5.1.7",
"postcss": "^8.4.12",
"snowpack": "^3.8.8",
"tailwindcss": "^3.0.8"
"tailwindcss": "^3.0.23"
}
}

File diff suppressed because it is too large Load diff

View file

@ -5,11 +5,15 @@ const colors = require('tailwindcss/colors');
*/
module.exports = {
content: ['./theme/**/*.ftl'],
experimental: {
optimizeUniversalDefaults: true,
},
plugins: [require('@tailwindcss/forms')],
theme: {
extend: {
colors: {
primary: colors.blue,
secondary: colors.gray,
},
},
},

View file

@ -1,10 +0,0 @@
<#macro kw component="span" rest...>
<${component}
class="absolute left-0 ml-3 text-lg"
<#list rest as attrName, attrValue>
${attrName}="${attrValue}"
</#list>
>
<#nested>
</${component}>
</#macro>

View file

@ -0,0 +1,10 @@
<#macro kw component="button" rest...>
<${component}
class="bg-secondary-100 flex justify-center px-4 py-2 relative rounded-lg text-sm text-secondary-600 w-full focus:outline-none focus:ring-2 focus:ring-secondary-600 focus:ring-offset-2 hover:bg-secondary-200 hover:text-secondary-900"
<#list rest as attrName, attrValue>
${attrName}="${attrValue}"
</#list>
>
<#nested>
</${component}>
</#macro>

View file

@ -0,0 +1,5 @@
<#macro kw>
<#compress>
${msg("authenticatorCode")} *
</#compress>
</#macro>

View file

@ -0,0 +1,5 @@
<#macro kw>
<#compress>
${msg("loginTotpDeviceName")} <#if totp.otpCredentials?size gte 1>*</#if>
</#compress>
</#macro>

View file

@ -1,16 +1,15 @@
<#import "../button/icon.ftl" as buttonIcon >
<#import "../button/primary.ftl" as buttonPrimary>
<#import "../icon/external-link.ftl" as iconExternalLink>
<#import "../link/primary.ftl" as linkPrimary>
<#macro kw>
<#nested "show-username">
<div class="mb-4">
<div class="font-bold mb-2 text-center">${auth.attemptedUsername}</div>
<@buttonPrimary.kw component="a" href="${url.loginRestartFlowUrl}">
<@buttonIcon.kw>
<@iconExternalLink.kw />
</@buttonIcon.kw>
${msg("restartLoginTooltip")}
</@buttonPrimary.kw>
<div class="flex items-center justify-center mb-4 space-x-2">
<b>${auth.attemptedUsername}</b>
<@linkPrimary.kw
href="${url.loginRestartFlowUrl}"
title="${msg('restartLoginTooltip')}"
>
<@iconExternalLink.kw />
</@linkPrimary.kw>
</div>
</#macro>

View file

@ -1,6 +1,6 @@
<#macro kw component="a" rest...>
<${component}
class="text-primary-600 hover:text-primary-500"
class="flex text-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-600 focus:ring-offset-2 hover:text-primary-500"
<#list rest as attrName, attrValue>
${attrName}="${attrValue}"
</#list>

View file

@ -1,6 +1,6 @@
<#macro kw component="a" rest...>
<${component}
class="text-gray-600 hover:text-black"
class="flex text-secondary-600 focus:outline-none focus:ring-2 focus:ring-secondary-600 focus:ring-offset-2 hover:text-secondary-900"
<#list rest as attrName, attrValue>
${attrName}="${attrValue}"
</#list>

View file

@ -0,0 +1,20 @@
<#macro kw id tabIndex checked=false rest...>
<div>
<input
<#if checked>checked</#if>
class="border-gray-300 focus:ring-primary-600"
id="${id}"
type="radio"
<#list rest as attrName, attrValue>
${attrName}="${attrValue}"
</#list>
>
<label
class="font-medium ml-2 text-sm"
for="${id}"
tabindex="${tabIndex}"
>
<#nested>
</label>
</div>
</#macro>

View file

@ -0,0 +1,113 @@
<#import "template.ftl" as layout>
<#import "components/button/primary.ftl" as buttonPrimary>
<#import "components/button/secondary.ftl" as buttonSecondary>
<#import "components/input/primary.ftl" as inputPrimary>
<#import "components/label/totp.ftl" as labelTotp>
<#import "components/label/userdevice.ftl" as labelUserDevice>
<#import "components/link/primary.ftl" as linkPrimary>
<@layout.registrationLayout
displayMessage=!messagesPerField.existsError("totp", "userLabel")
displayRequiredFields=false
;
section
>
<#if section="header">
${msg("loginTotpTitle")}
<#elseif section="form">
<ol class="list-decimal pl-4 space-y-2">
<li>
<p>${msg("loginTotpStep1")}</p>
<ul class="list-disc pl-6 py-2 space-y-2">
<#list totp.policy.supportedApplications as app>
<li>${app}</li>
</#list>
</ul>
</li>
<#if mode?? && mode = "manual">
<li>
<p>${msg("loginTotpManualStep2")}</p>
<p class="font-bold py-2 text-xl">${totp.totpSecretEncoded}</p>
</li>
<li>
<@linkPrimary.kw href=totp.qrUrl>
${msg("loginTotpScanBarcode")}
</@linkPrimary.kw>
</li>
<li>
<p>${msg("loginTotpManualStep3")}</p>
<ul class="list-disc pl-6 py-2 space-y-2">
<li>${msg("loginTotpType")}: ${msg("loginTotp." + totp.policy.type)}</li>
<li>${msg("loginTotpAlgorithm")}: ${totp.policy.getAlgorithmKey()}</li>
<li>${msg("loginTotpDigits")}: ${totp.policy.digits}</li>
<#if totp.policy.type = "totp">
<li>${msg("loginTotpInterval")}: ${totp.policy.period}</li>
<#elseif totp.policy.type = "hotp">
<li>${msg("loginTotpCounter")}: ${totp.policy.initialCounter}</li>
</#if>
</ul>
</li>
<#else>
<li>
<p>${msg("loginTotpStep2")}</p>
<img
alt="Figure: Barcode"
class="mx-auto"
src="data:image/png;base64, ${totp.totpSecretQrCode}"
>
<@linkPrimary.kw href=totp.manualUrl>
${msg("loginTotpUnableToScan")}
</@linkPrimary.kw>
</li>
</#if>
<li>${msg("loginTotpStep3")}</li>
<li>${msg("loginTotpStep3DeviceName")}</li>
</ol>
<form action="${url.loginAction}" class="m-0 space-y-4" method="post">
<div>
<@inputPrimary.kw
autocomplete="off"
autofocus=true
invalid=["totp"]
name="totp"
required=false
type="text"
>
<@labelTotp.kw />
</@inputPrimary.kw>
<input name="totpSecret" type="hidden" value="${totp.totpSecret}">
<#if mode??>
<input name="mode" type="hidden" value="${mode}">
</#if>
</div>
<div>
<@inputPrimary.kw
autocomplete="off"
invalid=["userLabel"]
name="userLabel"
required=false
type="text"
>
<@labelUserDevice.kw />
</@inputPrimary.kw>
</div>
<#if isAppInitiatedAction??>
<div class="flex flex-col pt-4 space-y-2">
<@buttonPrimary.kw type="submit">
${msg("doSubmit")}
</@buttonPrimary.kw>
<@buttonSecondary.kw name="cancel-aia" type="submit">
${msg("doCancel")}
</@buttonSecondary.kw>
</div>
<#else>
<div class="pt-4">
<@buttonPrimary.kw type="submit">
${msg("doSubmit")}
</@buttonPrimary.kw>
</div>
</#if>
</form>
</#if>
</@layout.registrationLayout>

View file

@ -0,0 +1,57 @@
<#import "template.ftl" as layout>
<#import "components/button/primary.ftl" as buttonPrimary>
<#import "components/input/primary.ftl" as inputPrimary>
<#import "components/label/totp.ftl" as labelTotp>
<#import "components/link/secondary.ftl" as linkSecondary>
<#import "components/radio/primary.ftl" as radioPrimary>
<@layout.registrationLayout
displayMessage=!messagesPerField.existsError("totp")
;
section
>
<#if section="header">
${msg("doLogIn")}
<#elseif section="form">
<form
action="${url.loginAction}"
class="m-0 space-y-4"
method="post"
>
<#if otpLogin.userOtpCredentials?size gt 1>
<div class="flex items-center space-x-4">
<#list otpLogin.userOtpCredentials as otpCredential>
<@radioPrimary.kw
checked=(otpCredential.id == otpLogin.selectedCredentialId)
id="kw-otp-credential-${otpCredential?index}"
name="selectedCredentialId"
tabIndex="${otpCredential?index}"
value="${otpCredential.id}"
>
${otpCredential.userLabel}
</@radioPrimary.kw>
</#list>
</div>
</#if>
<div>
<@inputPrimary.kw
autocomplete="off"
autofocus=true
invalid=["totp"]
name="otp"
type="text"
>
<@labelTotp.kw />
</@inputPrimary.kw>
</div>
<div class="pt-4">
<@buttonPrimary.kw
name="submitAction"
type="submit"
>
${msg("doLogIn")}
</@buttonPrimary.kw>
</div>
</form>
</#if>
</@layout.registrationLayout>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long