mirror of
https://github.com/lukin/keywind.git
synced 2025-01-09 01:16:25 +00:00
feat: add login otp pages (#9)
Co-authored-by: Anthony Lukin <anthony@lukin.dev>
This commit is contained in:
parent
259f10aae6
commit
a3f1c1d1d1
17 changed files with 678 additions and 463 deletions
|
@ -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
|
||||
```
|
||||
|
|
14
package.json
14
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
858
pnpm-lock.yaml
858
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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>
|
10
theme/keywind/login/components/button/secondary.ftl
Normal file
10
theme/keywind/login/components/button/secondary.ftl
Normal 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>
|
5
theme/keywind/login/components/label/totp.ftl
Normal file
5
theme/keywind/login/components/label/totp.ftl
Normal file
|
@ -0,0 +1,5 @@
|
|||
<#macro kw>
|
||||
<#compress>
|
||||
${msg("authenticatorCode")} *
|
||||
</#compress>
|
||||
</#macro>
|
5
theme/keywind/login/components/label/userdevice.ftl
Normal file
5
theme/keywind/login/components/label/userdevice.ftl
Normal file
|
@ -0,0 +1,5 @@
|
|||
<#macro kw>
|
||||
<#compress>
|
||||
${msg("loginTotpDeviceName")} <#if totp.otpCredentials?size gte 1>*</#if>
|
||||
</#compress>
|
||||
</#macro>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
20
theme/keywind/login/components/radio/primary.ftl
Normal file
20
theme/keywind/login/components/radio/primary.ftl
Normal 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>
|
113
theme/keywind/login/login-config-totp.ftl
Normal file
113
theme/keywind/login/login-config-totp.ftl
Normal 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>
|
57
theme/keywind/login/login-otp.ftl
Normal file
57
theme/keywind/login/login-otp.ftl
Normal 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>
|
2
theme/keywind/login/resources/dist/index.css
vendored
2
theme/keywind/login/resources/dist/index.css
vendored
File diff suppressed because one or more lines are too long
7
theme/keywind/login/resources/dist/index.js
vendored
7
theme/keywind/login/resources/dist/index.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue