Merge branch 'master' into pr/32

This commit is contained in:
@lukin 2023-03-27 00:00:00 +04:00
commit dd7d40cca0
38 changed files with 1568 additions and 160 deletions

3
.gitignore vendored
View file

@ -1,5 +1,8 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# build
out/
# dependencies
node_modules/

View file

@ -6,20 +6,32 @@ Keywind is a component-based Keycloak Login Theme built with [Tailwind CSS](http
### Styled Pages
- Error
- Login
- Login Config TOTP
- Login IDP Link Confirm
- Login OAuth Grant
- Login OTP
- Login Page Expired
- Login Password
- Login Recovery Authn Code Config
- Login Recovery Authn Code Input
- Login Reset Password
- Login Update Password
- Login Update Profile
- Login Username
- Logout Confirm
- Register
- Select Authenticator
- Terms and Conditions
- WebAuthn Authenticate
- WebAuthn Error
- WebAuthn Register
### Identity Provider Icons
- Bitbucket
- Discord
- Facebook
- GitHub
- GitLab
@ -30,6 +42,7 @@ Keywind is a component-based Keycloak Login Theme built with [Tailwind CSS](http
- OpenID
- Red Hat OpenShift
- PayPal
- Slack
- Stack Overflow
- Twitter
@ -45,7 +58,7 @@ Keywind has been designed with component-based architecture from the start, and
parent=keywind
```
4. Brand and customize components with [FreeMaker](https://freemarker.apache.org/docs/dgui_quickstart_template.html)
4. Brand and customize components with [FreeMarker](https://freemarker.apache.org/docs/dgui_quickstart_template.html)
## Customization
@ -91,3 +104,9 @@ When you're ready to deploy your own theme, run the build command to generate a
pnpm install
pnpm build
```
To deploy a theme as an archive, create a JAR archive with the theme resources.
```bash
pnpm build:jar
```

View file

@ -1,20 +1,27 @@
{
"$schema": "https://json.schemastore.org/package",
"name": "keywind",
"scripts": {
"build": "tsc && vite build",
"build:jar": "vite-node scripts/build",
"dev": "vite build --watch",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"alpinejs": "^3.10.5"
"alpinejs": "^3.12.0",
"rfc4648": "^1.5.2"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.3",
"@types/alpinejs": "^3.7.1",
"autoprefixer": "^10.4.13",
"postcss": "^8.4.20",
"tailwindcss": "^3.2.4",
"typescript": "^4.9.4",
"vite": "^4.0.1"
"@types/archiver": "^5.3.1",
"@types/node": "^18.15.0",
"archiver": "^5.3.1",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.21",
"tailwindcss": "^3.2.7",
"typescript": "^4.9.5",
"vite": "^4.1.4",
"vite-node": "^0.29.2"
}
}

File diff suppressed because it is too large Load diff

25
scripts/build.ts Normal file
View file

@ -0,0 +1,25 @@
import archiver from 'archiver';
import { createWriteStream, existsSync, mkdirSync } from 'fs';
import { name } from '../package.json';
const dir = 'out';
const file = `${name}.jar`;
const path = `${dir}/${file}`;
!existsSync(dir) && mkdirSync(dir);
const output = createWriteStream(`${__dirname}/../${path}`);
const archive = archiver('zip');
archive.on('error', (error) => {
throw error;
});
archive.pipe(output);
archive.directory('META-INF', 'META-INF');
archive.directory('theme', 'theme');
archive.finalize();

59
src/data/recoveryCodes.ts Normal file
View file

@ -0,0 +1,59 @@
import Alpine from 'alpinejs';
type DataType = {
$refs: RefsType;
$store: StoreType;
};
type RefsType = {
codeList: HTMLUListElement;
};
type StoreType = {
recoveryCodes: {
downloadFileDate: string;
downloadFileDescription: string;
downloadFileHeader: string;
downloadFileName: string;
};
};
document.addEventListener('alpine:init', () => {
Alpine.data('recoveryCodes', function (this: DataType) {
const { codeList } = this.$refs;
const { downloadFileDate, downloadFileDescription, downloadFileHeader, downloadFileName } =
this.$store.recoveryCodes;
const date = new Date().toLocaleString(navigator.language);
const codeElements = codeList.getElementsByTagName('li');
const codes = Array.from(codeElements)
.map((codeElement) => codeElement.innerText)
.join('\n');
return {
copy: () => navigator.clipboard.writeText(codes),
download: () => {
const element = document.createElement('a');
const text = `${downloadFileHeader}\n\n${codes}\n\n${downloadFileDescription}\n\n${downloadFileDate} ${date}`;
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', `${downloadFileName}.txt`);
element.click();
},
print: () => {
const codeListHTML = codeList.innerHTML;
const styles = 'div { font-family: monospace; list-style-type: none }';
const content = `<html><style>${styles}</style><body><title>${downloadFileName}</title><p>${downloadFileHeader}</p><div>${codeListHTML}</div><p>${downloadFileDescription}</p><p>${downloadFileDate} ${date}</p></body></html>`;
const printWindow = window.open();
if (printWindow) {
printWindow.document.write(content);
printWindow.print();
printWindow.close();
}
},
};
});
});

View file

@ -0,0 +1,147 @@
import Alpine from 'alpinejs';
import { base64url } from 'rfc4648';
type DataType = {
$refs: RefsType;
$store: StoreType;
};
type RefsType = {
authenticatorDataInput: HTMLInputElement;
authnSelectForm?: HTMLFormElement;
clientDataJSONInput: HTMLInputElement;
credentialIdInput: HTMLInputElement;
errorInput: HTMLInputElement;
signatureInput: HTMLInputElement;
userHandleInput: HTMLInputElement;
webAuthnForm: HTMLFormElement;
};
type StoreType = {
webAuthnAuthenticate: {
challenge: string;
createTimeout: string;
isUserIdentified: string;
rpId: string;
unsupportedBrowserText: string;
userVerification: UserVerificationRequirement | 'not specified';
};
};
document.addEventListener('alpine:init', () => {
Alpine.data('webAuthnAuthenticate', function (this: DataType) {
const {
authenticatorDataInput,
authnSelectForm,
clientDataJSONInput,
credentialIdInput,
errorInput,
signatureInput,
userHandleInput,
webAuthnForm,
} = this.$refs;
const {
challenge,
createTimeout,
isUserIdentified,
rpId,
unsupportedBrowserText,
userVerification,
} = this.$store.webAuthnAuthenticate;
const doAuthenticate = (allowCredentials: PublicKeyCredentialDescriptor[]) => {
if (!window.PublicKeyCredential) {
errorInput.value = unsupportedBrowserText;
webAuthnForm.submit();
return;
}
const publicKey: PublicKeyCredentialRequestOptions = {
challenge: base64url.parse(challenge, { loose: true }),
rpId: rpId,
};
if (allowCredentials.length) {
publicKey.allowCredentials = allowCredentials;
}
if (parseInt(createTimeout) !== 0) publicKey.timeout = parseInt(createTimeout) * 1000;
if (userVerification !== 'not specified') publicKey.userVerification = userVerification;
navigator.credentials
.get({ publicKey })
.then((result) => {
if (
result instanceof PublicKeyCredential &&
result.response instanceof AuthenticatorAssertionResponse
) {
window.result = result;
authenticatorDataInput.value = base64url.stringify(
new Uint8Array(result.response.authenticatorData),
{ pad: false }
);
clientDataJSONInput.value = base64url.stringify(
new Uint8Array(result.response.clientDataJSON),
{ pad: false }
);
signatureInput.value = base64url.stringify(new Uint8Array(result.response.signature), {
pad: false,
});
credentialIdInput.value = result.id;
if (result.response.userHandle) {
userHandleInput.value = base64url.stringify(
new Uint8Array(result.response.userHandle),
{ pad: false }
);
}
webAuthnForm.submit();
}
})
.catch((error) => {
errorInput.value = error;
webAuthnForm.submit();
});
};
const checkAllowCredentials = () => {
const allowCredentials: PublicKeyCredentialDescriptor[] = [];
if (authnSelectForm) {
const authnSelectFormElements = Array.from(authnSelectForm.elements);
if (authnSelectFormElements.length) {
authnSelectFormElements.forEach((element) => {
if (element instanceof HTMLInputElement) {
allowCredentials.push({
id: base64url.parse(element.value, { loose: true }),
type: 'public-key',
});
}
});
}
}
doAuthenticate(allowCredentials);
};
return {
webAuthnAuthenticate: () => {
if (!isUserIdentified) {
doAuthenticate([]);
return;
}
checkAllowCredentials();
},
};
});
});

View file

@ -0,0 +1,225 @@
import Alpine from 'alpinejs';
import { base64url } from 'rfc4648';
type DataType = {
$refs: RefsType;
$store: StoreType;
};
type RefsType = {
attestationObjectInput: HTMLInputElement;
authenticatorLabelInput: HTMLInputElement;
clientDataJSONInput: HTMLInputElement;
errorInput: HTMLInputElement;
publicKeyCredentialIdInput: HTMLInputElement;
registerForm: HTMLFormElement;
transportsInput: HTMLInputElement;
};
type StoreType = {
webAuthnRegister: {
attestationConveyancePreference: AttestationConveyancePreference | 'not specified';
authenticatorAttachment: AuthenticatorAttachment | 'not specified';
challenge: string;
createTimeout: string;
excludeCredentialIds: string;
requireResidentKey: string;
rpEntityName: string;
rpId: string;
signatureAlgorithms: string;
unsupportedBrowserText: string;
userId: string;
userVerificationRequirement: UserVerificationRequirement | 'not specified';
username: string;
};
};
document.addEventListener('alpine:init', () => {
Alpine.data('webAuthnRegister', function (this: DataType) {
const {
attestationObjectInput,
authenticatorLabelInput,
clientDataJSONInput,
errorInput,
publicKeyCredentialIdInput,
registerForm,
transportsInput,
} = this.$refs;
const {
attestationConveyancePreference,
authenticatorAttachment,
challenge,
createTimeout,
excludeCredentialIds,
requireResidentKey,
rpEntityName,
rpId,
signatureAlgorithms,
unsupportedBrowserText,
userId,
userVerificationRequirement,
username,
} = this.$store.webAuthnRegister;
const getPubKeyCredParams = (signatureAlgorithms: string) => {
const pubKeyCredParams: PublicKeyCredentialParameters[] = [];
if (signatureAlgorithms === '') {
pubKeyCredParams.push({ alg: -7, type: 'public-key' });
return pubKeyCredParams;
}
const signatureAlgorithmsList = signatureAlgorithms.split(',');
signatureAlgorithmsList.forEach((value) => {
pubKeyCredParams.push({
alg: parseInt(value),
type: 'public-key',
});
});
return pubKeyCredParams;
};
const getExcludeCredentials = (excludeCredentialIds: string) => {
const excludeCredentials: PublicKeyCredentialDescriptor[] = [];
if (excludeCredentialIds === '') return excludeCredentials;
const excludeCredentialIdsList = excludeCredentialIds.split(',');
excludeCredentialIdsList.forEach((value) => {
excludeCredentials.push({
id: base64url.parse(value, { loose: true }),
type: 'public-key',
});
});
return excludeCredentials;
};
const getTransportsAsString = (transportsList: string | string[]) => {
if (transportsList === '' || transportsList.constructor !== Array) return '';
let transportsString = '';
transportsList.forEach((value) => {
transportsString += value + ',';
});
return transportsString.slice(0, -1);
};
return {
registerSecurityKey: () => {
if (!window.PublicKeyCredential) {
errorInput.value = unsupportedBrowserText;
registerForm.submit();
return;
}
const publicKey: PublicKeyCredentialCreationOptions = {
challenge: base64url.parse(challenge, { loose: true }),
pubKeyCredParams: getPubKeyCredParams(signatureAlgorithms),
rp: {
id: rpId,
name: rpEntityName,
},
user: {
displayName: username,
id: base64url.parse(userId, { loose: true }),
name: username,
},
};
if (attestationConveyancePreference !== 'not specified')
publicKey.attestation = attestationConveyancePreference;
const authenticatorSelection: AuthenticatorSelectionCriteria = {};
let isAuthenticatorSelectionSpecified = false;
if (authenticatorAttachment !== 'not specified') {
authenticatorSelection.authenticatorAttachment = authenticatorAttachment;
isAuthenticatorSelectionSpecified = true;
}
if (requireResidentKey !== 'not specified') {
if (requireResidentKey === 'Yes') authenticatorSelection.requireResidentKey = true;
else authenticatorSelection.requireResidentKey = false;
isAuthenticatorSelectionSpecified = true;
}
if (userVerificationRequirement !== 'not specified') {
authenticatorSelection.userVerification = userVerificationRequirement;
isAuthenticatorSelectionSpecified = true;
}
if (isAuthenticatorSelectionSpecified)
publicKey.authenticatorSelection = authenticatorSelection;
const excludeCredentials = getExcludeCredentials(excludeCredentialIds);
if (excludeCredentials.length > 0) publicKey.excludeCredentials = excludeCredentials;
if (parseInt(createTimeout) !== 0) publicKey.timeout = parseInt(createTimeout) * 1000;
navigator.credentials
.create({ publicKey })
.then((result) => {
if (
result instanceof PublicKeyCredential &&
result.response instanceof AuthenticatorAttestationResponse
) {
const { getTransports } = result.response;
window.result = result;
const publicKeyCredentialId = result.rawId;
attestationObjectInput.value = base64url.stringify(
new Uint8Array(result.response.attestationObject),
{ pad: false }
);
clientDataJSONInput.value = base64url.stringify(
new Uint8Array(result.response.clientDataJSON),
{ pad: false }
);
publicKeyCredentialIdInput.value = base64url.stringify(
new Uint8Array(publicKeyCredentialId),
{ pad: false }
);
if (typeof getTransports === 'function') {
const transports = getTransports();
if (transports) {
transportsInput.value = getTransportsAsString(transports);
}
} else {
console.log(
'Your browser is not able to recognize supported transport media for the authenticator.'
);
}
const initLabel = 'WebAuthn Authenticator (Default Label)';
let labelResult = window.prompt(
"Please input your registered authenticator's label",
initLabel
);
if (labelResult === null) labelResult = initLabel;
authenticatorLabelInput.value = labelResult;
registerForm.submit();
}
})
.catch(function (error) {
error.value = error;
registerForm.submit();
});
},
};
});
});

1
src/global.d.ts vendored
View file

@ -3,5 +3,6 @@ import type { Alpine } from 'alpinejs';
declare global {
interface Window {
Alpine: Alpine;
result?: PublicKeyCredential;
}
}

View file

@ -17,6 +17,7 @@ module.exports = {
provider: {
bitbucket: '#0052CC',
discord: '#5865F2',
facebook: '#1877F2',
github: '#181717',
gitlab: '#FC6D26',
@ -27,6 +28,7 @@ module.exports = {
oidc: '#F78C40',
openshift: '#EE0000',
paypal: '#00457C',
slack: '#4A154B',
stackoverflow: '#F58025',
twitter: '#1DA1F2',
},

View file

@ -0,0 +1,7 @@
<#-- https://discord.com/branding -->
<#macro kw name="Discord">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>${name}</title>
<path d="M20.3303 4.42852C18.7535 3.70661 17.0888 3.19446 15.3789 2.90516C15.1449 3.32345 14.9332 3.75381 14.7446 4.19445C12.9232 3.91998 11.071 3.91998 9.24961 4.19445C9.06095 3.75386 8.84924 3.3235 8.61535 2.90516C6.90433 3.19691 5.2386 3.71027 3.66019 4.43229C0.526644 9.06843 -0.322811 13.5894 0.101917 18.0462C1.937 19.4021 3.99098 20.4332 6.17458 21.0948C6.66626 20.4335 7.10134 19.732 7.47519 18.9976C6.76511 18.7324 6.07975 18.4052 5.42706 18.0198C5.59884 17.8952 5.76684 17.7669 5.92918 17.6423C7.82837 18.5354 9.90124 18.9985 12 18.9985C14.0987 18.9985 16.1715 18.5354 18.0707 17.6423C18.235 17.7763 18.403 17.9047 18.5729 18.0198C17.9189 18.4058 17.2323 18.7337 16.5209 18.9995C16.8943 19.7335 17.3294 20.4345 17.8216 21.0948C20.007 20.4359 22.0626 19.4052 23.898 18.0481C24.3963 12.8797 23.0467 8.4002 20.3303 4.42852ZM8.01318 15.3053C6.82961 15.3053 5.85179 14.2312 5.85179 12.9099C5.85179 11.5885 6.79563 10.505 8.0094 10.505C9.22318 10.505 10.1934 11.5885 10.1727 12.9099C10.1519 14.2312 9.21941 15.3053 8.01318 15.3053ZM15.9867 15.3053C14.8013 15.3053 13.8272 14.2312 13.8272 12.9099C13.8272 11.5885 14.7711 10.505 15.9867 10.505C17.2024 10.505 18.1651 11.5885 18.1444 12.9099C18.1236 14.2312 17.193 15.3053 15.9867 15.3053Z" fill="#5865F2" />
</svg>
</#macro>

View file

@ -1,4 +1,5 @@
<#import "./bitbucket.ftl" as bitbucketIcon>
<#import "./discord.ftl" as discordIcon>
<#import "./facebook.ftl" as facebookIcon>
<#import "./github.ftl" as githubIcon>
<#import "./gitlab.ftl" as gitlabIcon>
@ -9,6 +10,7 @@
<#import "./oidc.ftl" as oidcIcon>
<#import "./openshift.ftl" as openshiftIcon>
<#import "./paypal.ftl" as paypalIcon>
<#import "./slack.ftl" as slackIcon>
<#import "./stackoverflow.ftl" as stackoverflowIcon>
<#import "./twitter.ftl" as twitterIcon>
@ -16,6 +18,10 @@
<@bitbucketIcon.kw />
</#macro>
<#macro discord>
<@discordIcon.kw />
</#macro>
<#macro facebook>
<@facebookIcon.kw />
</#macro>
@ -60,6 +66,10 @@
<@paypalIcon.kw />
</#macro>
<#macro slack>
<@slackIcon.kw />
</#macro>
<#macro stackoverflow>
<@stackoverflowIcon.kw />
</#macro>

View file

@ -0,0 +1,14 @@
<#-- https://slack.com/media-kit -->
<#macro kw name="Slack">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>${name}</title>
<path d="M5.04235 15.1661C5.04235 16.5537 3.9088 17.6873 2.52117 17.6873C1.13355 17.6873 0 16.5537 0 15.1661C0 13.7785 1.13355 12.645 2.52117 12.645H5.04235V15.1661Z" fill="#E01E5A" />
<path d="M6.3127 15.1661C6.3127 13.7785 7.44626 12.645 8.83388 12.645C10.2215 12.645 11.355 13.7785 11.355 15.1661V21.4788C11.355 22.8664 10.2215 24 8.83388 24C7.44626 24 6.3127 22.8664 6.3127 21.4788V15.1661Z" fill="#E01E5A" />
<path d="M8.83388 5.04235C7.44626 5.04235 6.3127 3.9088 6.3127 2.52117C6.3127 1.13355 7.44626 0 8.83388 0C10.2215 0 11.355 1.13355 11.355 2.52117V5.04235H8.83388Z" fill="#36C5F0" />
<path d="M8.83388 6.3127C10.2215 6.3127 11.355 7.44626 11.355 8.83388C11.355 10.2215 10.2215 11.355 8.83388 11.355H2.52117C1.13355 11.355 0 10.2215 0 8.83388C0 7.44626 1.13355 6.3127 2.52117 6.3127H8.83388Z" fill="#36C5F0" />
<path d="M18.9577 8.83388C18.9577 7.44626 20.0912 6.3127 21.4788 6.3127C22.8664 6.3127 24 7.44626 24 8.83388C24 10.2215 22.8664 11.355 21.4788 11.355H18.9577V8.83388Z" fill="#2EB67D" />
<path d="M17.6873 8.83388C17.6873 10.2215 16.5537 11.355 15.1661 11.355C13.7785 11.355 12.645 10.2215 12.645 8.83388V2.52117C12.645 1.13355 13.7785 0 15.1661 0C16.5537 0 17.6873 1.13355 17.6873 2.52117V8.83388Z" fill="#2EB67D" />
<path d="M15.1661 18.9577C16.5537 18.9577 17.6873 20.0912 17.6873 21.4788C17.6873 22.8664 16.5537 24 15.1661 24C13.7785 24 12.645 22.8664 12.645 21.4788V18.9577H15.1661Z" fill="#ECB22E" />
<path d="M15.1661 17.6873C13.7785 17.6873 12.645 16.5537 12.645 15.1661C12.645 13.7785 13.7785 12.645 15.1661 12.645H21.4788C22.8664 12.645 24 13.7785 24 15.1661C24 16.5537 22.8664 17.6873 21.4788 17.6873H15.1661Z" fill="#ECB22E" />
</svg>
</#macro>

View file

@ -10,6 +10,9 @@
<#case "bitbucket">
<#assign colorClass="hover:bg-provider-bitbucket/10">
<#break>
<#case "discord">
<#assign colorClass="hover:bg-provider-discord/10">
<#break>
<#case "facebook">
<#assign colorClass="hover:bg-provider-facebook/10">
<#break>
@ -43,6 +46,9 @@
<#case "paypal">
<#assign colorClass="hover:bg-provider-paypal/10">
<#break>
<#case "slack">
<#assign colorClass="hover:bg-provider-slack/10">
<#break>
<#case "stackoverflow">
<#assign colorClass="hover:bg-provider-stackoverflow/10">
<#break>

View file

@ -3,7 +3,7 @@
<#macro kw linkHref="" linkTitle="" name="">
<div class="flex items-center justify-center mb-4 space-x-2">
<b>${name}</b>
<span class="font-medium">${name}</span>
<@link.kw
color="primary"
href=linkHref

View file

@ -1,25 +1,25 @@
<#macro kw script="">
<title>${msg("loginTitle", (realm.displayName!""))}</title>
<meta charset="utf-8" />
<meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charset="utf-8">
<meta name="robots" content="noindex, nofollow">
<meta name="viewport" content="width=device-width, initial-scale=1">
<#if properties.meta?has_content>
<#list properties.meta?split(" ") as meta>
<meta name="${meta?split('==')[0]}" content="${meta?split('==')[1]}"/>
<meta name="${meta?split('==')[0]}" content="${meta?split('==')[1]}">
</#list>
</#if>
<#if properties.favicons?has_content>
<#list properties.favicons?split(" ") as favicon>
<link href="${url.resourcesPath}/${favicon?split('==')[0]}" rel="${meta?split('==')[1]}">
<link href="${url.resourcesPath}/${favicon?split('==')[0]}" rel="${favicon?split('==')[1]}">
</#list>
</#if>
<#if properties.styles?has_content>
<#list properties.styles?split(" ") as style>
<link href="${url.resourcesPath}/${style}" rel="stylesheet" />
<link href="${url.resourcesPath}/${style}" rel="stylesheet">
</#list>
</#if>

View file

@ -0,0 +1,18 @@
<#import "template.ftl" as layout>
<#import "components/atoms/alert.ftl" as alert>
<#import "components/atoms/link.ftl" as link>
<@layout.registrationLayout displayMessage=false; section>
<#if section="header">
${kcSanitize(msg("errorTitle"))?no_esc}
<#elseif section="form">
<@alert.kw color="error">${kcSanitize(message.summary)?no_esc}</@alert.kw>
<#if !skipLink??>
<#if client?? && client.baseUrl?has_content>
<@link.kw color="secondary" href=client.baseUrl size="small">
${kcSanitize(msg("backToApplication"))?no_esc}
</@link.kw>
</#if>
</#if>
</#if>
</@layout.registrationLayout>

View file

@ -31,7 +31,7 @@
<#if mode?? && mode="manual">
<li>
<p>${msg("loginTotpManualStep2")}</p>
<p class="font-bold text-xl">${totp.totpSecretEncoded}</p>
<p class="font-medium text-xl">${totp.totpSecretEncoded}</p>
</li>
<li>
<@link.kw color="primary" href=totp.qrUrl>

View file

@ -0,0 +1,18 @@
<#import "template.ftl" as layout>
<#import "components/atoms/button.ftl" as button>
<#import "components/atoms/button-group.ftl" as buttonGroup>
<@layout.registrationLayout; section>
<#if section="header">
${msg("pageExpiredTitle")}
<#elseif section="form">
<@buttonGroup.kw>
<@button.kw color="primary" component="a" href=url.loginRestartFlowUrl>
${msg("doTryAgain")}
</@button.kw>
<@button.kw color="secondary" component="a" href=url.loginAction>
${msg("doContinue")}
</@button.kw>
</@buttonGroup.kw>
</#if>
</@layout.registrationLayout>

View file

@ -0,0 +1,39 @@
<#import "template.ftl" as layout>
<#import "components/atoms/button.ftl" as button>
<#import "components/atoms/button-group.ftl" as buttonGroup>
<#import "components/atoms/form.ftl" as form>
<#import "components/atoms/input.ftl" as input>
<#import "components/atoms/link.ftl" as link>
<@layout.registrationLayout displayMessage=!messagesPerField.existsError("password"); section>
<#if section="header">
${msg("doLogIn")}
<#elseif section="form">
<@form.kw
action=url.loginAction
method="post"
onsubmit="login.disabled = true; return true;"
>
<@input.kw
autofocus=true
invalid=messagesPerField.existsError("password")
label=msg("password")
message=kcSanitize(messagesPerField.get("password"))?no_esc
name="password"
type="password"
/>
<#if realm.resetPasswordAllowed>
<div class="flex items-center justify-between">
<@link.kw color="primary" href=url.loginResetCredentialsUrl size="small">
${msg("doForgotPassword")}
</@link.kw>
</div>
</#if>
<@buttonGroup.kw>
<@button.kw color="primary" name="login" type="submit">
${msg("doLogIn")}
</@button.kw>
</@buttonGroup.kw>
</@form.kw>
</#if>
</@layout.registrationLayout>

View file

@ -0,0 +1,91 @@
<#import "template.ftl" as layout>
<#import "components/atoms/alert.ftl" as alert>
<#import "components/atoms/button.ftl" as button>
<#import "components/atoms/button-group.ftl" as buttonGroup>
<#import "components/atoms/checkbox.ftl" as checkbox>
<#import "components/atoms/form.ftl" as form>
<@layout.registrationLayout script="dist/recoveryCodes.js"; section>
<#if section="header">
${msg("recovery-code-config-header")}
<#elseif section="form">
<div class="space-y-6" x-data="recoveryCodes">
<@alert.kw color="warning">
<div class="space-y-2">
<h4 class="font-medium">${msg("recovery-code-config-warning-title")}</h4>
<p>${msg("recovery-code-config-warning-message")}</p>
</div>
</@alert.kw>
<ul class="columns-2 font-mono text-center" x-ref="codeList">
<#list recoveryAuthnCodesConfigBean.generatedRecoveryAuthnCodesList as code>
<li>${code[0..3]}-${code[4..7]}-${code[8..]}</li>
</#list>
</ul>
<div class="flex items-stretch space-x-4 mb-4">
<@button.kw @click="print" color="secondary" size="small" type="button">
${msg("recovery-codes-print")}
</@button.kw>
<@button.kw @click="download" color="secondary" size="small" type="button">
${msg("recovery-codes-download")}
</@button.kw>
<@button.kw @click="copy" color="secondary" size="small" type="button">
${msg("recovery-codes-copy")}
</@button.kw>
</div>
<@form.kw action=url.loginAction method="post">
<input
name="generatedRecoveryAuthnCodes"
type="hidden"
value="${recoveryAuthnCodesConfigBean.generatedRecoveryAuthnCodesAsString}"
/>
<input
name="generatedAt"
type="hidden"
value="${recoveryAuthnCodesConfigBean.generatedAt?c}"
/>
<input
name="userLabel"
type="hidden"
value="${msg('recovery-codes-label-default')}"
/>
<@checkbox.kw
label=msg("recovery-codes-confirmation-message")
name="kcRecoveryCodesConfirmationCheck"
required="required"
x\-ref="confirmationCheck"
/>
<@buttonGroup.kw>
<#if isAppInitiatedAction??>
<@button.kw color="primary" type="submit">
${msg("recovery-codes-action-complete")}
</@button.kw>
<@button.kw
@click="$refs.confirmationCheck.required = false"
color="secondary"
name="cancel-aia"
type="submit"
value="true"
>
${msg("recovery-codes-action-cancel")}
</@button.kw>
<#else>
<@button.kw color="primary" type="submit">
${msg("recovery-codes-action-complete")}
</@button.kw>
</#if>
</@buttonGroup.kw>
</@form.kw>
</div>
</#if>
</@layout.registrationLayout>
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('recoveryCodes', {
downloadFileDate: '${msg("recovery-codes-download-file-date")}',
downloadFileDescription: '${msg("recovery-codes-download-file-description")}',
downloadFileHeader: '${msg("recovery-codes-download-file-header")}',
downloadFileName: 'kc-download-recovery-codes',
})
})
</script>

View file

@ -0,0 +1,26 @@
<#import "template.ftl" as layout>
<#import "components/atoms/button.ftl" as button>
<#import "components/atoms/button-group.ftl" as buttonGroup>
<#import "components/atoms/form.ftl" as form>
<#import "components/atoms/input.ftl" as input>
<@layout.registrationLayout; section>
<#if section="header">
${msg("auth-recovery-code-header")}
<#elseif section="form">
<@form.kw action=url.loginAction method="post">
<@input.kw
autocomplete="off"
autofocus=true
label=msg("auth-recovery-code-prompt", recoveryAuthnCodesInputBean.codeNumber?c)
name="recoveryCodeInput"
type="text"
/>
<@buttonGroup.kw>
<@button.kw color="primary" name="login" type="submit">
${msg("doLogIn")}
</@button.kw>
</@buttonGroup.kw>
</@form.kw>
</#if>
</@layout.registrationLayout>

View file

@ -0,0 +1,71 @@
<#import "template.ftl" as layout>
<#import "components/atoms/button.ftl" as button>
<#import "components/atoms/button-group.ftl" as buttonGroup>
<#import "components/atoms/checkbox.ftl" as checkbox>
<#import "components/atoms/form.ftl" as form>
<#import "components/atoms/input.ftl" as input>
<#import "components/atoms/link.ftl" as link>
<#import "components/molecules/identity-provider.ftl" as identityProvider>
<#import "features/labels/username.ftl" as usernameLabel>
<#assign usernameLabel><@usernameLabel.kw /></#assign>
<@layout.registrationLayout
displayInfo=realm.password && realm.registrationAllowed && !registrationDisabled??
displayMessage=!messagesPerField.existsError("username")
;
section
>
<#if section="header">
${msg("loginAccountTitle")}
<#elseif section="form">
<#if realm.password>
<@form.kw
action=url.loginAction
method="post"
onsubmit="login.disabled = true; return true;"
>
<#if !usernameHidden??>
<@input.kw
autocomplete=realm.loginWithEmailAllowed?string("email", "username")
autofocus=true
disabled=usernameEditDisabled??
invalid=messagesPerField.existsError("username")
label=usernameLabel
message=kcSanitize(messagesPerField.get("username"))?no_esc
name="username"
type="text"
value=(login.username)!''
/>
</#if>
<#if realm.rememberMe && !usernameHidden??>
<div class="flex items-center justify-between">
<@checkbox.kw
checked=login.rememberMe??
label=msg("rememberMe")
name="rememberMe"
/>
</div>
</#if>
<@buttonGroup.kw>
<@button.kw color="primary" name="login" type="submit">
${msg("doLogIn")}
</@button.kw>
</@buttonGroup.kw>
</@form.kw>
</#if>
<#elseif section="info">
<#if realm.password && realm.registrationAllowed && !registrationDisabled??>
<div class="text-center">
${msg("noAccount")}
<@link.kw color="primary" href=url.registrationUrl>
${msg("doRegister")}
</@link.kw>
</div>
</#if>
<#elseif section="socialProviders">
<#if realm.password && social.providers??>
<@identityProvider.kw providers=social.providers />
</#if>
</#if>
</@layout.registrationLayout>

View file

@ -70,9 +70,6 @@
</@buttonGroup.kw>
</@form.kw>
</#if>
<#if realm.password && social.providers??>
<@identityProvider.kw providers=social.providers />
</#if>
<#elseif section="info">
<#if realm.password && realm.registrationAllowed && !registrationDisabled??>
<div class="text-center">
@ -82,5 +79,9 @@
</@link.kw>
</div>
</#if>
<#elseif section="socialProviders">
<#if realm.password && social.providers??>
<@identityProvider.kw providers=social.providers />
</#if>
</#if>
</@layout.registrationLayout>

View file

@ -0,0 +1 @@
var s={};Object.defineProperty(s,"__esModule",{value:!0});function v(e,r,a){var l;if(a===void 0&&(a={}),!r.codes){r.codes={};for(var n=0;n<r.chars.length;++n)r.codes[r.chars[n]]=n}if(!a.loose&&e.length*r.bits&7)throw new SyntaxError("Invalid padding");for(var b=e.length;e[b-1]==="=";)if(--b,!a.loose&&!((e.length-b)*r.bits&7))throw new SyntaxError("Invalid padding");for(var c=new((l=a.out)!=null?l:Uint8Array)(b*r.bits/8|0),t=0,i=0,u=0,f=0;f<b;++f){var h=r.codes[e[f]];if(h===void 0)throw new SyntaxError("Invalid character "+e[f]);i=i<<r.bits|h,t+=r.bits,t>=8&&(t-=8,c[u++]=255&i>>t)}if(t>=r.bits||255&i<<8-t)throw new SyntaxError("Unexpected end of data");return c}function o(e,r,a){a===void 0&&(a={});for(var l=a,n=l.pad,b=n===void 0?!0:n,c=(1<<r.bits)-1,t="",i=0,u=0,f=0;f<e.length;++f)for(u=u<<8|255&e[f],i+=8;i>r.bits;)i-=r.bits,t+=r.chars[c&u>>i];if(i&&(t+=r.chars[c&u<<r.bits-i]),b)for(;t.length*r.bits&7;)t+="=";return t}var p={chars:"0123456789ABCDEF",bits:4},y={chars:"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",bits:5},d={chars:"0123456789ABCDEFGHIJKLMNOPQRSTUV",bits:5},w={chars:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",bits:6},x={chars:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",bits:6},E={parse:function(r,a){return v(r.toUpperCase(),p,a)},stringify:function(r,a){return o(r,p,a)}},$={parse:function(r,a){return a===void 0&&(a={}),v(a.loose?r.toUpperCase().replace(/0/g,"O").replace(/1/g,"L").replace(/8/g,"B"):r,y,a)},stringify:function(r,a){return o(r,y,a)}},U={parse:function(r,a){return v(r,d,a)},stringify:function(r,a){return o(r,d,a)}},S={parse:function(r,a){return v(r,w,a)},stringify:function(r,a){return o(r,w,a)}},g={parse:function(r,a){return v(r,x,a)},stringify:function(r,a){return o(r,x,a)}},C={parse:v,stringify:o};s.base16=E;s.base32=$;s.base32hex=U;s.base64=S;s.base64url=g;s.codec=C;s.base16;s.base32;s.base32hex;s.base64;const I=s.base64url;s.codec;export{I as b};

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

View file

@ -0,0 +1,8 @@
import{m as p}from"./assets/module.esm-62c37d0d.js";document.addEventListener("alpine:init",()=>{p.data("recoveryCodes",function(){const{codeList:o}=this.$refs,{downloadFileDate:n,downloadFileDescription:i,downloadFileHeader:d,downloadFileName:s}=this.$store.recoveryCodes,a=new Date().toLocaleString(navigator.language),r=o.getElementsByTagName("li"),l=Array.from(r).map(t=>t.innerText).join(`
`);return{copy:()=>navigator.clipboard.writeText(l),download:()=>{const t=document.createElement("a"),c=`${d}
${l}
${i}
${n} ${a}`;t.setAttribute("href","data:text/plain;charset=utf-8,"+encodeURIComponent(c)),t.setAttribute("download",`${s}.txt`),t.click()},print:()=>{const t=o.innerHTML,m=`<html><style>div { font-family: monospace; list-style-type: none }</style><body><title>${s}</title><p>${d}</p><div>${t}</div><p>${i}</p><p>${n} ${a}</p></body></html>`,e=window.open();e&&(e.document.write(m),e.print(),e.close())}}})});

View file

@ -0,0 +1 @@
import{m as g}from"./assets/module.esm-62c37d0d.js";import{b as n}from"./assets/index-a7b84447.js";document.addEventListener("alpine:init",()=>{g.data("webAuthnAuthenticate",function(){const{authenticatorDataInput:l,authnSelectForm:r,clientDataJSONInput:p,credentialIdInput:f,errorInput:s,signatureInput:d,userHandleInput:h,webAuthnForm:a}=this.$refs,{challenge:m,createTimeout:o,isUserIdentified:A,rpId:w,unsupportedBrowserText:y,userVerification:u}=this.$store.webAuthnAuthenticate,c=i=>{if(!window.PublicKeyCredential){s.value=y,a.submit();return}const t={challenge:n.parse(m,{loose:!0}),rpId:w};i.length&&(t.allowCredentials=i),parseInt(o)!==0&&(t.timeout=parseInt(o)*1e3),u!=="not specified"&&(t.userVerification=u),navigator.credentials.get({publicKey:t}).then(e=>{e instanceof PublicKeyCredential&&e.response instanceof AuthenticatorAssertionResponse&&(window.result=e,l.value=n.stringify(new Uint8Array(e.response.authenticatorData),{pad:!1}),p.value=n.stringify(new Uint8Array(e.response.clientDataJSON),{pad:!1}),d.value=n.stringify(new Uint8Array(e.response.signature),{pad:!1}),f.value=e.id,e.response.userHandle&&(h.value=n.stringify(new Uint8Array(e.response.userHandle),{pad:!1})),a.submit())}).catch(e=>{s.value=e,a.submit()})},b=()=>{const i=[];if(r){const t=Array.from(r.elements);t.length&&t.forEach(e=>{e instanceof HTMLInputElement&&i.push({id:n.parse(e.value,{loose:!0}),type:"public-key"})})}c(i)};return{webAuthnAuthenticate:()=>{if(!A){c([]);return}b()}}})});

View file

@ -0,0 +1 @@
import{m as T}from"./assets/module.esm-62c37d0d.js";import{b as a}from"./assets/index-a7b84447.js";document.addEventListener("alpine:init",()=>{T.data("webAuthnRegister",function(){const{attestationObjectInput:m,authenticatorLabelInput:g,clientDataJSONInput:A,errorInput:I,publicKeyCredentialIdInput:w,registerForm:s,transportsInput:C}=this.$refs,{attestationConveyancePreference:u,authenticatorAttachment:c,challenge:K,createTimeout:l,excludeCredentialIds:v,requireResidentKey:p,rpEntityName:S,rpId:P,signatureAlgorithms:R,unsupportedBrowserText:x,userId:E,userVerificationRequirement:d,username:f}=this.$store.webAuthnRegister,L=t=>{const e=[];return t===""?(e.push({alg:-7,type:"public-key"}),e):(t.split(",").forEach(r=>{e.push({alg:parseInt(r),type:"public-key"})}),e)},q=t=>{const e=[];return t===""||t.split(",").forEach(r=>{e.push({id:a.parse(r,{loose:!0}),type:"public-key"})}),e},N=t=>{if(t===""||t.constructor!==Array)return"";let e="";return t.forEach(i=>{e+=i+","}),e.slice(0,-1)};return{registerSecurityKey:()=>{if(!window.PublicKeyCredential){I.value=x,s.submit();return}const t={challenge:a.parse(K,{loose:!0}),pubKeyCredParams:L(R),rp:{id:P,name:S},user:{displayName:f,id:a.parse(E,{loose:!0}),name:f}};u!=="not specified"&&(t.attestation=u);const e={};let i=!1;c!=="not specified"&&(e.authenticatorAttachment=c,i=!0),p!=="not specified"&&(p==="Yes"?e.requireResidentKey=!0:e.requireResidentKey=!1,i=!0),d!=="not specified"&&(e.userVerification=d,i=!0),i&&(t.authenticatorSelection=e);const r=q(v);r.length>0&&(t.excludeCredentials=r),parseInt(l)!==0&&(t.timeout=parseInt(l)*1e3),navigator.credentials.create({publicKey:t}).then(n=>{if(n instanceof PublicKeyCredential&&n.response instanceof AuthenticatorAttestationResponse){const{getTransports:h}=n.response;window.result=n;const O=n.rawId;if(m.value=a.stringify(new Uint8Array(n.response.attestationObject),{pad:!1}),A.value=a.stringify(new Uint8Array(n.response.clientDataJSON),{pad:!1}),w.value=a.stringify(new Uint8Array(O),{pad:!1}),typeof h=="function"){const b=h();b&&(C.value=N(b))}else console.log("Your browser is not able to recognize supported transport media for the authenticator.");const y="WebAuthn Authenticator (Default Label)";let o=window.prompt("Please input your registered authenticator's label",y);o===null&&(o=y),g.value=o,s.submit()}}).catch(function(n){n.value=n,s.submit()})}}})});

View file

@ -0,0 +1,28 @@
<#import "template.ftl" as layout>
<#import "components/atoms/form.ftl" as form>
<#import "components/atoms/link.ftl" as link>
<@layout.registrationLayout displayInfo=false; section>
<#if section="header">
${msg("loginChooseAuthenticator")}
<#elseif section="form">
<div x-data>
<@form.kw action=url.loginAction method="post" x\-ref="selectCredentialForm">
<input name="authenticationExecution" type="hidden" x-ref="authExecInput" />
<#list auth.authenticationSelections as authenticationSelection>
<div>
<@link.kw
@click="$refs.authExecInput.value = '${authenticationSelection.authExecId}'; $refs.selectCredentialForm.submit()"
color="primary"
component="button"
type="button"
>
${msg("${authenticationSelection.displayName}")}
</@link.kw>
<div class="text-sm">${msg("${authenticationSelection.helpText}")}</div>
</div>
</#list>
</@form.kw>
</div>
</#if>
</@layout.registrationLayout>

View file

@ -56,6 +56,7 @@
</@button.kw>
</form>
</#if>
<#nested "socialProviders">
</#assign>
<#assign cardFooter>

View file

@ -0,0 +1,22 @@
<#import "template.ftl" as layout>
<#import "components/atoms/button.ftl" as button>
<#import "components/atoms/button-group.ftl" as buttonGroup>
<#import "components/atoms/form.ftl" as form>
<@layout.registrationLayout displayMessage=false; section>
<#if section="header">
${msg("termsTitle")}
<#elseif section="form">
${kcSanitize(msg("termsText"))?no_esc}
<@form.kw action=url.loginAction method="post">
<@buttonGroup.kw>
<@button.kw color="primary" name="accept" type="submit">
${msg("doAccept")}
</@button.kw>
<@button.kw color="secondary" name="cancel" type="submit">
${msg("doDecline")}
</@button.kw>
</@buttonGroup.kw>
</@form.kw>
</#if>
</@layout.registrationLayout>

View file

@ -0,0 +1,71 @@
<#import "template.ftl" as layout>
<#import "components/atoms/button.ftl" as button>
<#import "components/atoms/button-group.ftl" as buttonGroup>
<@layout.registrationLayout script="dist/webAuthnAuthenticate.js"; section>
<#if section="title">
title
<#elseif section="header">
${kcSanitize(msg("webauthn-login-title"))?no_esc}
<#elseif section="form">
<div x-data="webAuthnAuthenticate">
<form action="${url.loginAction}" method="post" x-ref="webAuthnForm">
<input name="authenticatorData" type="hidden" x-ref="authenticatorDataInput" />
<input name="clientDataJSON" type="hidden" x-ref="clientDataJSONInput" />
<input name="credentialId" type="hidden" x-ref="credentialIdInput" />
<input name="error" type="hidden" x-ref="errorInput" />
<input name="signature" type="hidden" x-ref="signatureInput" />
<input name="userHandle" type="hidden" x-ref="userHandleInput" />
</form>
<#if authenticators??>
<form x-ref="authnSelectForm">
<#list authenticators.authenticators as authenticator>
<input value="${authenticator.credentialId}" type="hidden" />
</#list>
</form>
<#if shouldDisplayAuthenticators?? && shouldDisplayAuthenticators>
<#if authenticators.authenticators?size gt 1>
<p>${kcSanitize(msg("webauthn-available-authenticators"))?no_esc}</p>
</#if>
<#list authenticators.authenticators as authenticator>
<div>
<div class="font-medium">${kcSanitize(msg("${authenticator.label}"))?no_esc}</div>
<#if authenticator.transports?? && authenticator.transports.displayNameProperties?has_content>
<div>
<#list authenticator.transports.displayNameProperties as nameProperty>
<span>${kcSanitize(msg("${nameProperty!}"))?no_esc}</span>
<#if nameProperty?has_next>
<span>, </span>
</#if>
</#list>
</div>
</#if>
<div class="text-sm">
<span>${kcSanitize(msg("webauthn-createdAt-label"))?no_esc}</span>
<span>${kcSanitize(authenticator.createdAt)?no_esc}</span>
</div>
</div>
</#list>
</#if>
</#if>
<@buttonGroup.kw>
<@button.kw @click="webAuthnAuthenticate" color="primary" type="button">
${kcSanitize(msg("webauthn-doAuthenticate"))}
</@button.kw>
</@buttonGroup.kw>
</div>
</#if>
</@layout.registrationLayout>
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('webAuthnAuthenticate', {
challenge: '${challenge}',
createTimeout: '${createTimeout}',
isUserIdentified: '${isUserIdentified}',
rpId: '${rpId}',
unsupportedBrowserText: '${msg("webauthn-unsupported-browser-text")?no_esc}',
userVerification: '${userVerification}',
})
})
</script>

View file

@ -0,0 +1,34 @@
<#import "template.ftl" as layout>
<#import "components/atoms/button.ftl" as button>
<#import "components/atoms/button-group.ftl" as buttonGroup>
<@layout.registrationLayout displayMessage=true; section>
<#if section="header">
${kcSanitize(msg("webauthn-error-title"))?no_esc}
<#elseif section="form">
<div x-data>
<form action="${url.loginAction}" method="post" x-ref="errorCredentialForm">
<input name="authenticationExecution" type="hidden" x-ref="executionValueInput" />
<input name="isSetRetry" type="hidden" x-ref="isSetRetryInput" />
</form>
<@buttonGroup.kw>
<@button.kw
@click="$refs.executionValueInput.value = '${execution}'; $refs.isSetRetryInput.value = 'retry'; $refs.errorCredentialForm.submit()"
color="primary"
name="try-again"
tabindex="4"
type="button"
>
${kcSanitize(msg("doTryAgain"))?no_esc}
</@button.kw>
<#if isAppInitiatedAction??>
<form action="${url.loginAction}" method="post">
<@button.kw color="secondary" name="cancel-aia" type="submit" value="true">
${msg("doCancel")}
</@button.kw>
</form>
</#if>
</@buttonGroup.kw>
</div>
</#if>
</@layout.registrationLayout>

View file

@ -0,0 +1,54 @@
<#import "template.ftl" as layout>
<#import "components/atoms/button.ftl" as button>
<#import "components/atoms/button-group.ftl" as buttonGroup>
<@layout.registrationLayout script="dist/webAuthnRegister.js"; section>
<#if section="title">
title
<#elseif section="header">
${kcSanitize(msg("webauthn-registration-title"))?no_esc}
<#elseif section="form">
<div x-data="webAuthnRegister">
<form action="${url.loginAction}" method="post" x-ref="registerForm">
<input name="attestationObject" type="hidden" x-ref="attestationObjectInput" />
<input name="authenticatorLabel" type="hidden" x-ref="authenticatorLabelInput" />
<input name="clientDataJSON" type="hidden" x-ref="clientDataJSONInput" />
<input name="error" type="hidden" x-ref="errorInput" />
<input name="publicKeyCredentialId" type="hidden" x-ref="publicKeyCredentialIdInput" />
<input name="transports" type="hidden" x-ref="transportsInput" />
</form>
<@buttonGroup.kw>
<@button.kw @click="registerSecurityKey" color="primary" type="submit">
${msg("doRegister")}
</@button.kw>
<#if !isSetRetry?has_content && isAppInitiatedAction?has_content>
<form action="${url.loginAction}" method="post">
<@button.kw color="secondary" name="cancel-aia" type="submit" value="true">
${msg("doCancel")}
</@button.kw>
</form>
</#if>
</@buttonGroup.kw>
</div>
</#if>
</@layout.registrationLayout>
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('webAuthnRegister', {
attestationConveyancePreference: '${attestationConveyancePreference}',
authenticatorAttachment: '${authenticatorAttachment}',
challenge: '${challenge}',
createTimeout: '${createTimeout}',
excludeCredentialIds: '${excludeCredentialIds}',
requireResidentKey: '${requireResidentKey}',
rpEntityName: '${rpEntityName}',
rpId: '${rpId}',
signatureAlgorithms: '${signatureAlgorithms}',
unsupportedBrowserText: '${msg("webauthn-unsupported-browser-text")?no_esc}',
userId: '${userid}',
userVerificationRequirement: '${userVerificationRequirement}',
username: '${username}',
})
})
</script>

View file

@ -3,7 +3,12 @@ import { defineConfig } from 'vite';
export default defineConfig({
build: {
rollupOptions: {
input: 'src/index.ts',
input: [
'src/index.ts',
'src/data/recoveryCodes.ts',
'src/data/webAuthnAuthenticate.ts',
'src/data/webAuthnRegister.ts',
],
output: {
assetFileNames: '[name][extname]',
dir: 'theme/keywind/login/resources/dist',