mirror of
https://github.com/lukin/keywind.git
synced 2025-01-09 09:26:24 +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
|
### Styled Pages
|
||||||
|
|
||||||
- Login
|
- Login
|
||||||
|
- Login Config TOTP
|
||||||
- Login IDP Link Confirm
|
- Login IDP Link Confirm
|
||||||
|
- Login OTP
|
||||||
- Login Reset Password
|
- Login Reset Password
|
||||||
- Login Update Password
|
- Login Update Password
|
||||||
- Login Update Profile
|
- 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.
|
When you're ready to deploy your own theme, run the build command to generate a static production build.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
pnpm install
|
||||||
npm run build
|
pnpm build
|
||||||
# or
|
|
||||||
yarn install
|
|
||||||
yarn build
|
|
||||||
```
|
```
|
||||||
|
|
14
package.json
14
package.json
|
@ -5,16 +5,16 @@
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"alpinejs": "^3.7.1"
|
"alpinejs": "^3.9.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@snowpack/plugin-postcss": "^1.4.3",
|
"@snowpack/plugin-postcss": "^1.4.3",
|
||||||
"@tailwindcss/forms": "^0.4.0",
|
"@tailwindcss/forms": "^0.5.0",
|
||||||
"@types/tailwindcss": "^3.0.0",
|
"@types/tailwindcss": "^3.0.10",
|
||||||
"autoprefixer": "^10.4.1",
|
"autoprefixer": "^10.4.4",
|
||||||
"cssnano": "^5.0.14",
|
"cssnano": "^5.1.7",
|
||||||
"postcss": "^8.4.5",
|
"postcss": "^8.4.12",
|
||||||
"snowpack": "^3.8.8",
|
"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 = {
|
module.exports = {
|
||||||
content: ['./theme/**/*.ftl'],
|
content: ['./theme/**/*.ftl'],
|
||||||
|
experimental: {
|
||||||
|
optimizeUniversalDefaults: true,
|
||||||
|
},
|
||||||
plugins: [require('@tailwindcss/forms')],
|
plugins: [require('@tailwindcss/forms')],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
primary: colors.blue,
|
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 "../icon/external-link.ftl" as iconExternalLink>
|
||||||
|
<#import "../link/primary.ftl" as linkPrimary>
|
||||||
|
|
||||||
<#macro kw>
|
<#macro kw>
|
||||||
<#nested "show-username">
|
<#nested "show-username">
|
||||||
<div class="mb-4">
|
<div class="flex items-center justify-center mb-4 space-x-2">
|
||||||
<div class="font-bold mb-2 text-center">${auth.attemptedUsername}</div>
|
<b>${auth.attemptedUsername}</b>
|
||||||
<@buttonPrimary.kw component="a" href="${url.loginRestartFlowUrl}">
|
<@linkPrimary.kw
|
||||||
<@buttonIcon.kw>
|
href="${url.loginRestartFlowUrl}"
|
||||||
<@iconExternalLink.kw />
|
title="${msg('restartLoginTooltip')}"
|
||||||
</@buttonIcon.kw>
|
>
|
||||||
${msg("restartLoginTooltip")}
|
<@iconExternalLink.kw />
|
||||||
</@buttonPrimary.kw>
|
</@linkPrimary.kw>
|
||||||
</div>
|
</div>
|
||||||
</#macro>
|
</#macro>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<#macro kw component="a" rest...>
|
<#macro kw component="a" rest...>
|
||||||
<${component}
|
<${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>
|
<#list rest as attrName, attrValue>
|
||||||
${attrName}="${attrValue}"
|
${attrName}="${attrValue}"
|
||||||
</#list>
|
</#list>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<#macro kw component="a" rest...>
|
<#macro kw component="a" rest...>
|
||||||
<${component}
|
<${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>
|
<#list rest as attrName, attrValue>
|
||||||
${attrName}="${attrValue}"
|
${attrName}="${attrValue}"
|
||||||
</#list>
|
</#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