├── .nvmrc ├── .husky ├── .gitignore └── pre-commit ├── src ├── login │ ├── index.ts │ ├── i18n │ │ ├── index.ts │ │ ├── ng-package.json │ │ └── i18n.ts │ ├── KcContext │ │ ├── index.ts │ │ ├── KcContext.ts │ │ └── ng-package.json │ ├── pages │ │ ├── code │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ ├── code.component.html │ │ │ └── code.component.ts │ │ ├── info │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── info.component.html │ │ ├── error │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ ├── error.component.html │ │ │ └── error.component.ts │ │ ├── login │ │ │ ├── index.ts │ │ │ └── ng-package.json │ │ ├── terms │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ ├── terms.component.html │ │ │ └── terms.component.ts │ │ ├── login-otp │ │ │ ├── index.ts │ │ │ └── ng-package.json │ │ ├── register │ │ │ ├── index.ts │ │ │ └── ng-package.json │ │ ├── update-email │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── update-email.component.html │ │ ├── login-password │ │ │ ├── index.ts │ │ │ └── ng-package.json │ │ ├── login-username │ │ │ ├── index.ts │ │ │ └── ng-package.json │ │ ├── logout-confirm │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ ├── logout-confirm.component.ts │ │ │ └── logout-confirm.component.html │ │ ├── saml-post-form │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── saml-post-form.component.html │ │ ├── webauthn-error │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── webauthn-error.component.html │ │ ├── delete-credential │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ ├── delete-credential.component.html │ │ │ └── delete-credential.component.ts │ │ ├── login-config-totp │ │ │ ├── index.ts │ │ │ └── ng-package.json │ │ ├── login-oauth-grant │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── login-oauth-grant.component.ts │ │ ├── login-reset-otp │ │ │ ├── index.ts │ │ │ └── ng-package.json │ │ ├── login-x509-info │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── login-x509-info.component.ts │ │ ├── webauthn-register │ │ │ ├── index.ts │ │ │ └── ng-package.json │ │ ├── frontchannel-logout │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── frontchannel-logout.component.html │ │ ├── login-page-expired │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ ├── login-page-expired.component.html │ │ │ └── login-page-expired.component.ts │ │ ├── login-verify-email │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ ├── login-verify-email.component.html │ │ │ └── login-verify-email.component.ts │ │ ├── delete-account-confirm │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── delete-account-confirm.component.html │ │ ├── login-idp-link-confirm │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ ├── login-idp-link-confirm.component.html │ │ │ └── login-idp-link-confirm.component.ts │ │ ├── login-idp-link-email │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ ├── login-idp-link-email.component.html │ │ │ └── login-idp-link-email.component.ts │ │ ├── login-reset-password │ │ │ ├── index.ts │ │ │ └── ng-package.json │ │ ├── login-update-password │ │ │ ├── index.ts │ │ │ └── ng-package.json │ │ ├── login-update-profile │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── login-update-profile.component.html │ │ ├── select-authenticator │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ ├── select-authenticator.component.html │ │ │ └── select-authenticator.component.ts │ │ ├── webauthn-authenticate │ │ │ ├── index.ts │ │ │ └── ng-package.json │ │ ├── idp-review-user-profile │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── idp-review-user-profile.component.html │ │ ├── login-idp-link-confirm-override │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ ├── login-idp-link-confirm-override.component.html │ │ │ └── login-idp-link-confirm-override.component.ts │ │ ├── login-recovery-authn-code-config │ │ │ ├── index.ts │ │ │ └── ng-package.json │ │ ├── login-recovery-authn-code-input │ │ │ ├── index.ts │ │ │ └── ng-package.json │ │ ├── login-oauth2-device-verify-user-code │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── login-oauth2-device-verify-user-code.component.html │ │ └── login-passkeys-conditional-authenticate │ │ │ ├── index.ts │ │ │ └── ng-package.json │ ├── services │ │ ├── i18n │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── i18n.service.ts │ │ ├── user-profile-form │ │ │ ├── index.ts │ │ │ └── ng-package.json │ │ └── login-resource-injector │ │ │ ├── index.ts │ │ │ └── ng-package.json │ ├── tokens │ │ ├── i18n │ │ │ ├── index.ts │ │ │ ├── i18n.token.ts │ │ │ └── ng-package.json │ │ ├── classes │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── classes.token.ts │ │ ├── kc-context │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── kc-context.token.ts │ │ └── make-user-confirm-password │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── make-user-confirm-password.token.ts │ ├── template │ │ ├── index.ts │ │ └── ng-package.json │ ├── components │ │ ├── input-tag │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── input-tag.component.html │ │ ├── field-errors │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ ├── field-errors.component.html │ │ │ └── field-errors.component.ts │ │ ├── group-label │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ ├── group-label.component.html │ │ │ └── group-label.component.ts │ │ ├── select-tag │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── select-tag.component.html │ │ ├── textarea-tag │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── textarea-tag.component.html │ │ ├── input-tag-selects │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── input-tag-selects.component.html │ │ ├── password-wrapper │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── password-wrapper.component.html │ │ ├── input-field-by-type │ │ │ ├── index.ts │ │ │ └── ng-package.json │ │ ├── logout-other-sessions │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ ├── logout-other-sessions.component.html │ │ │ └── logout-other-sessions.component.ts │ │ ├── user-profile-form-fields │ │ │ ├── index.ts │ │ │ └── ng-package.json │ │ └── add-remove-buttons-multi-valued-attribute │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── add-remove-buttons-multi-valued-attribute.component.html │ ├── directives │ │ └── kc-class │ │ │ ├── index.ts │ │ │ └── ng-package.json │ ├── classes │ │ └── component-reference │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── component-reference.class.ts │ ├── providers │ │ └── keycloakify-angular │ │ │ ├── index.ts │ │ │ └── ng-package.json │ ├── ng-package.json │ └── login.ts ├── account │ ├── i18n │ │ ├── index.ts │ │ ├── ng-package.json │ │ └── i18n.ts │ ├── index.ts │ ├── KcContext │ │ ├── index.ts │ │ ├── KcContext.ts │ │ └── ng-package.json │ ├── DefaultPage │ │ ├── index.ts │ │ └── ng-package.json │ ├── pages │ │ ├── log │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ ├── log.component.html │ │ │ └── log.component.ts │ │ ├── totp │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── totp.component.ts │ │ ├── account │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── account.component.ts │ │ ├── password │ │ │ ├── index.ts │ │ │ └── ng-package.json │ │ ├── sessions │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ ├── sessions.component.ts │ │ │ └── sessions.component.html │ │ ├── applications │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── applications.component.ts │ │ └── federatedIdentity │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── federatedIdentity.component.ts │ ├── tokens │ │ ├── i18n │ │ │ ├── index.ts │ │ │ ├── i18n.token.ts │ │ │ └── ng-package.json │ │ ├── classes │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── classes.token.ts │ │ └── kc-context │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── kc-context.token.ts │ ├── services │ │ ├── i18n │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── i18n.service.ts │ │ └── account-resource-injector │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── account-resource-injector.service.ts │ ├── template │ │ ├── index.ts │ │ └── ng-package.json │ ├── directives │ │ └── kc-class │ │ │ ├── index.ts │ │ │ └── ng-package.json │ ├── classes │ │ └── component-reference │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── component-reference.class.ts │ ├── providers │ │ └── keycloakify-angular │ │ │ ├── index.ts │ │ │ └── ng-package.json │ ├── ng-package.json │ ├── account.ts │ └── defaultPage.ts ├── lib │ ├── models │ │ └── script │ │ │ ├── index.ts │ │ │ ├── script.model.ts │ │ │ └── ng-package.json │ ├── pipes │ │ ├── to-array │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── to-array.pipe.ts │ │ ├── input-type │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── input-type.pipe.ts │ │ ├── to-number │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── to-number.pipe.ts │ │ ├── kc-sanitize │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── kc-sanitize.pipe.ts │ │ └── is-array-with-empty-object │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── is-array-with-empty-object.pipe.ts │ ├── directives │ │ └── attributes │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── attributes.directive.ts │ ├── index.ts │ ├── tokens │ │ └── use-default-css │ │ │ ├── index.ts │ │ │ ├── use-default-css.token.ts │ │ │ └── ng-package.json │ ├── services │ │ └── resource-injector │ │ │ ├── index.ts │ │ │ ├── ng-package.json │ │ │ └── resource-injector.service.ts │ └── ng-package.json ├── index.ts ├── bin │ ├── initialize-account-theme │ │ ├── index.ts │ │ └── boilerplate │ │ │ ├── i18n.ts │ │ │ ├── KcContext.ts │ │ │ └── KcPage.ts │ ├── tools │ │ ├── kebabCaseToSnakeCase.ts │ │ ├── getThisCodebaseRootDirPath.ts │ │ ├── fs.rmSync.ts │ │ ├── crawl.ts │ │ └── String.prototype.replaceAll.ts │ ├── core.ts │ ├── tsconfig.json │ └── main.ts ├── eslint.config.js ├── ng-package.json ├── tsconfig.lib.prod.json ├── tsconfig.lib.json └── package.json ├── stories ├── kc.gen.ts ├── account │ ├── KcContext.ts │ ├── i18n.ts │ ├── KcContextMock.ts │ ├── KcPage.ts │ └── KcPageStory.ts └── login │ ├── KcContext.ts │ ├── pages │ ├── saml-post-form.stories.ts │ ├── login-oauth2-device-verify-user-code.stories.ts │ ├── login-recovery-authn-code-input.stories.ts │ ├── login-idp-link-confirm-override.stories.ts │ ├── login-passkeys-conditional-authenticate.stories.ts │ ├── frontchannel-logout.stories.ts │ ├── login-username.stories.ts │ ├── update-email.stories.ts │ ├── login-page-expired.stories.ts │ ├── login-update-profile.stories.ts │ ├── login-recovery-authn-code-config.stories.ts │ ├── delete-account-confirm.stories.ts │ ├── webauthn-register.stories.ts │ ├── code.stories.ts │ ├── login-x509-info.stories.ts │ ├── login-update-password.stories.ts │ ├── logout-confirm.stories.ts │ ├── error.stories.ts │ ├── login-reset-password.stories.ts │ ├── login-password.stories.ts │ ├── delete-credential.stories.ts │ ├── login-config-totp.stories.ts │ ├── webauthn-error.stories.ts │ ├── login-oauth-grant.stories.ts │ └── idp-review-user-profile.stories.ts │ ├── i18n.ts │ ├── KcContextMock.ts │ ├── KcPage.ts │ └── KcPageStory.ts ├── scripts ├── shared │ ├── run.ts │ └── cleanup.ts ├── tools │ ├── fs.existsAsync.ts │ ├── getThisCodebaseRootDirPath.ts │ ├── removeNodeModules.ts │ ├── Deferred.ts │ ├── fs.rm.ts │ ├── waitForThrottle.ts │ └── StatefulObservable.ts └── build.ts ├── .prettierignore ├── prettier.config.js ├── .gitignore ├── LICENSE ├── tsconfig.json └── .all-contributorsrc /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/krypton -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /src/login/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login'; 2 | -------------------------------------------------------------------------------- /src/account/i18n/index.ts: -------------------------------------------------------------------------------- 1 | export * from './i18n'; 2 | -------------------------------------------------------------------------------- /src/account/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account'; 2 | -------------------------------------------------------------------------------- /src/login/i18n/index.ts: -------------------------------------------------------------------------------- 1 | export * from './i18n'; 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | npm run format && npm run lint -------------------------------------------------------------------------------- /src/account/KcContext/index.ts: -------------------------------------------------------------------------------- 1 | export * from './KcContext'; 2 | -------------------------------------------------------------------------------- /src/login/KcContext/index.ts: -------------------------------------------------------------------------------- 1 | export * from './KcContext'; 2 | -------------------------------------------------------------------------------- /src/account/DefaultPage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DefaultPage'; 2 | -------------------------------------------------------------------------------- /src/account/pages/log/index.ts: -------------------------------------------------------------------------------- 1 | export * from './log.component'; 2 | -------------------------------------------------------------------------------- /src/account/tokens/i18n/index.ts: -------------------------------------------------------------------------------- 1 | export * from './i18n.token'; 2 | -------------------------------------------------------------------------------- /src/lib/models/script/index.ts: -------------------------------------------------------------------------------- 1 | export * from './script.model'; 2 | -------------------------------------------------------------------------------- /src/lib/pipes/to-array/index.ts: -------------------------------------------------------------------------------- 1 | export * from './to-array.pipe'; 2 | -------------------------------------------------------------------------------- /src/login/pages/code/index.ts: -------------------------------------------------------------------------------- 1 | export * from './code.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/info/index.ts: -------------------------------------------------------------------------------- 1 | export * from './info.component'; 2 | -------------------------------------------------------------------------------- /src/login/services/i18n/index.ts: -------------------------------------------------------------------------------- 1 | export * from './i18n.service'; 2 | -------------------------------------------------------------------------------- /src/login/tokens/i18n/index.ts: -------------------------------------------------------------------------------- 1 | export * from './i18n.token'; 2 | -------------------------------------------------------------------------------- /src/account/pages/totp/index.ts: -------------------------------------------------------------------------------- 1 | export * from './totp.component'; 2 | -------------------------------------------------------------------------------- /src/account/services/i18n/index.ts: -------------------------------------------------------------------------------- 1 | export * from './i18n.service'; 2 | -------------------------------------------------------------------------------- /src/account/template/index.ts: -------------------------------------------------------------------------------- 1 | export * from './template.component'; 2 | -------------------------------------------------------------------------------- /src/account/tokens/classes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './classes.token'; 2 | -------------------------------------------------------------------------------- /src/lib/pipes/input-type/index.ts: -------------------------------------------------------------------------------- 1 | export * from './input-type.pipe'; 2 | -------------------------------------------------------------------------------- /src/lib/pipes/to-number/index.ts: -------------------------------------------------------------------------------- 1 | export * from './to-number.pipe'; 2 | -------------------------------------------------------------------------------- /src/login/pages/error/index.ts: -------------------------------------------------------------------------------- 1 | export * from './error.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/login/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/terms/index.ts: -------------------------------------------------------------------------------- 1 | export * from './terms.component'; 2 | -------------------------------------------------------------------------------- /src/login/template/index.ts: -------------------------------------------------------------------------------- 1 | export * from './template.component'; 2 | -------------------------------------------------------------------------------- /src/login/tokens/classes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './classes.token'; 2 | -------------------------------------------------------------------------------- /src/account/pages/account/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account.component'; 2 | -------------------------------------------------------------------------------- /src/account/pages/password/index.ts: -------------------------------------------------------------------------------- 1 | export * from './password.component'; 2 | -------------------------------------------------------------------------------- /src/account/pages/sessions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sessions.component'; 2 | -------------------------------------------------------------------------------- /src/account/tokens/kc-context/index.ts: -------------------------------------------------------------------------------- 1 | export * from './kc-context.token'; 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export const KEYCLOAKIFY_ANGULAR = '@keycloakify/angular'; 2 | -------------------------------------------------------------------------------- /src/lib/pipes/kc-sanitize/index.ts: -------------------------------------------------------------------------------- 1 | export * from './kc-sanitize.pipe'; 2 | -------------------------------------------------------------------------------- /src/login/pages/login-otp/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login-otp.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/register/index.ts: -------------------------------------------------------------------------------- 1 | export * from './register.component'; 2 | -------------------------------------------------------------------------------- /src/login/tokens/kc-context/index.ts: -------------------------------------------------------------------------------- 1 | export * from './kc-context.token'; 2 | -------------------------------------------------------------------------------- /src/account/directives/kc-class/index.ts: -------------------------------------------------------------------------------- 1 | export * from './kc-class.directive'; 2 | -------------------------------------------------------------------------------- /src/lib/directives/attributes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './attributes.directive'; 2 | -------------------------------------------------------------------------------- /src/login/components/input-tag/index.ts: -------------------------------------------------------------------------------- 1 | export * from './input-tag.component'; 2 | -------------------------------------------------------------------------------- /src/login/directives/kc-class/index.ts: -------------------------------------------------------------------------------- 1 | export * from './kc-class.directive'; 2 | -------------------------------------------------------------------------------- /src/login/pages/update-email/index.ts: -------------------------------------------------------------------------------- 1 | export * from './update-email.component'; 2 | -------------------------------------------------------------------------------- /src/account/pages/applications/index.ts: -------------------------------------------------------------------------------- 1 | export * from './applications.component'; 2 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export const KEYCLOAKIFY_ANGULAR_LIB = '@keycloakify/angular/lib'; 2 | -------------------------------------------------------------------------------- /src/lib/tokens/use-default-css/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-default-css.token'; 2 | -------------------------------------------------------------------------------- /src/login/components/field-errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './field-errors.component'; 2 | -------------------------------------------------------------------------------- /src/login/components/group-label/index.ts: -------------------------------------------------------------------------------- 1 | export * from './group-label.component'; 2 | -------------------------------------------------------------------------------- /src/login/components/select-tag/index.ts: -------------------------------------------------------------------------------- 1 | export * from './select-tag.component'; 2 | -------------------------------------------------------------------------------- /src/login/components/textarea-tag/index.ts: -------------------------------------------------------------------------------- 1 | export * from './textarea-tag.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/login-password/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login-password.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/login-username/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login-username.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/logout-confirm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logout-confirm.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/saml-post-form/index.ts: -------------------------------------------------------------------------------- 1 | export * from './saml-post-form.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/webauthn-error/index.ts: -------------------------------------------------------------------------------- 1 | export * from './webauthn-error.component'; 2 | -------------------------------------------------------------------------------- /src/bin/initialize-account-theme/index.ts: -------------------------------------------------------------------------------- 1 | export * from './initialize-account-theme'; 2 | -------------------------------------------------------------------------------- /src/lib/services/resource-injector/index.ts: -------------------------------------------------------------------------------- 1 | export * from './resource-injector.service'; 2 | -------------------------------------------------------------------------------- /src/login/pages/delete-credential/index.ts: -------------------------------------------------------------------------------- 1 | export * from './delete-credential.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/login-config-totp/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login-config-totp.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/login-oauth-grant/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login-oauth-grant.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/login-reset-otp/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login-reset-otp.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/login-x509-info/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login-x509-info.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/webauthn-register/index.ts: -------------------------------------------------------------------------------- 1 | export * from './webauthn-register.component'; 2 | -------------------------------------------------------------------------------- /src/account/classes/component-reference/index.ts: -------------------------------------------------------------------------------- 1 | export * from './component-reference.class'; 2 | -------------------------------------------------------------------------------- /src/account/pages/federatedIdentity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './federatedIdentity.component'; 2 | -------------------------------------------------------------------------------- /src/login/KcContext/KcContext.ts: -------------------------------------------------------------------------------- 1 | export type { KcContext } from 'keycloakify/login/KcContext'; 2 | -------------------------------------------------------------------------------- /src/login/classes/component-reference/index.ts: -------------------------------------------------------------------------------- 1 | export * from './component-reference.class'; 2 | -------------------------------------------------------------------------------- /src/login/components/input-tag-selects/index.ts: -------------------------------------------------------------------------------- 1 | export * from './input-tag-selects.component'; 2 | -------------------------------------------------------------------------------- /src/login/components/password-wrapper/index.ts: -------------------------------------------------------------------------------- 1 | export * from './password-wrapper.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/frontchannel-logout/index.ts: -------------------------------------------------------------------------------- 1 | export * from './frontchannel-logout.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/login-page-expired/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login-page-expired.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/login-verify-email/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login-verify-email.component'; 2 | -------------------------------------------------------------------------------- /src/login/services/user-profile-form/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user-profile-form.service'; 2 | -------------------------------------------------------------------------------- /src/account/KcContext/KcContext.ts: -------------------------------------------------------------------------------- 1 | export type { KcContext } from 'keycloakify/account/KcContext'; 2 | -------------------------------------------------------------------------------- /src/account/providers/keycloakify-angular/index.ts: -------------------------------------------------------------------------------- 1 | export * from './keycloakify-angular.providers'; 2 | -------------------------------------------------------------------------------- /src/login/components/input-field-by-type/index.ts: -------------------------------------------------------------------------------- 1 | export * from './input-field-by-type.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/delete-account-confirm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './delete-account-confirm.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/login-idp-link-confirm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login-idp-link-confirm.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/login-idp-link-email/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login-idp-link-email.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/login-reset-password/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login-reset-password.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/login-update-password/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login-update-password.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/login-update-profile/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login-update-profile.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/select-authenticator/index.ts: -------------------------------------------------------------------------------- 1 | export * from './select-authenticator.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/webauthn-authenticate/index.ts: -------------------------------------------------------------------------------- 1 | export * from './webauthn-authenticate.component'; 2 | -------------------------------------------------------------------------------- /src/login/providers/keycloakify-angular/index.ts: -------------------------------------------------------------------------------- 1 | export * from './keycloakify-angular.providers'; 2 | -------------------------------------------------------------------------------- /src/lib/pipes/is-array-with-empty-object/index.ts: -------------------------------------------------------------------------------- 1 | export * from './is-array-with-empty-object.pipe'; 2 | -------------------------------------------------------------------------------- /src/login/components/logout-other-sessions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logout-other-sessions.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/idp-review-user-profile/index.ts: -------------------------------------------------------------------------------- 1 | export * from './idp-review-user-profile.component'; 2 | -------------------------------------------------------------------------------- /src/login/services/login-resource-injector/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login-resource-injector.service'; 2 | -------------------------------------------------------------------------------- /src/login/tokens/make-user-confirm-password/index.ts: -------------------------------------------------------------------------------- 1 | export * from './make-user-confirm-password.token'; 2 | -------------------------------------------------------------------------------- /src/account/services/account-resource-injector/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account-resource-injector.service'; 2 | -------------------------------------------------------------------------------- /src/login/components/user-profile-form-fields/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user-profile-form-fields.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/login-idp-link-confirm-override/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login-idp-link-confirm-override.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/login-recovery-authn-code-config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login-recovery-authn-code-config.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/login-recovery-authn-code-input/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login-recovery-authn-code-input.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/login-oauth2-device-verify-user-code/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login-oauth2-device-verify-user-code.component'; 2 | -------------------------------------------------------------------------------- /src/login/pages/login-passkeys-conditional-authenticate/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login-passkeys-conditional-authenticate.component'; 2 | -------------------------------------------------------------------------------- /src/login/components/add-remove-buttons-multi-valued-attribute/index.ts: -------------------------------------------------------------------------------- 1 | export * from './add-remove-buttons-multi-valued-attribute.component'; 2 | -------------------------------------------------------------------------------- /src/lib/models/script/script.model.ts: -------------------------------------------------------------------------------- 1 | export type Script = { 2 | type: string; 3 | id: string; 4 | src?: string; 5 | textContent?: string; 6 | }; 7 | -------------------------------------------------------------------------------- /src/login/tokens/i18n/i18n.token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | export const LOGIN_I18N = new InjectionToken('login i18n'); 4 | -------------------------------------------------------------------------------- /src/account/tokens/i18n/i18n.token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | export const ACCOUNT_I18N = new InjectionToken('account i18n'); 4 | -------------------------------------------------------------------------------- /src/lib/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/account/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/i18n/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/account/i18n/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/KcContext/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/account/DefaultPage/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/account/KcContext/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/account/pages/log/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/account/template/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { defineConfig } from 'eslint/config'; 3 | import rootConfig from '../eslint.config.js'; 4 | 5 | export default defineConfig(...rootConfig); 6 | -------------------------------------------------------------------------------- /src/lib/models/script/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/code/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/error/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/info/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/login/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/terms/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/template/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/tokens/i18n/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/account/pages/account/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/account/pages/password/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/account/pages/sessions/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/account/pages/totp/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/account/services/i18n/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/account/tokens/classes/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/account/tokens/i18n/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/pipes/input-type/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/pipes/kc-sanitize/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/pipes/to-array/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/pipes/to-number/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/tokens/use-default-css/use-default-css.token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | export const USE_DEFAULT_CSS = new InjectionToken('use default css'); 4 | -------------------------------------------------------------------------------- /src/login/pages/login-otp/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/register/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/services/i18n/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/tokens/classes/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/account/directives/kc-class/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/account/pages/applications/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/account/tokens/kc-context/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/directives/attributes/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/tokens/use-default-css/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/components/input-tag/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/components/select-tag/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/directives/kc-class/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/login-password/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/login-reset-otp/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/login-username/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/login-x509-info/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/logout-confirm/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/saml-post-form/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/update-email/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/webauthn-error/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/tokens/kc-context/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/account/pages/federatedIdentity/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/services/resource-injector/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/components/field-errors/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/components/group-label/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/components/textarea-tag/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/delete-credential/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/frontchannel-logout/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/login-config-totp/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/login-idp-link-email/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/login-oauth-grant/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/login-page-expired/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/login-reset-password/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/login-update-profile/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/login-verify-email/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/select-authenticator/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/webauthn-register/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/services/user-profile-form/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/account/classes/component-reference/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/account/providers/keycloakify-angular/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/pipes/is-array-with-empty-object/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/classes/component-reference/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/components/input-field-by-type/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/components/input-tag-selects/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/components/password-wrapper/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/delete-account-confirm/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/idp-review-user-profile/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/login-idp-link-confirm/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/login-update-password/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/webauthn-authenticate/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/providers/keycloakify-angular/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/account/services/account-resource-injector/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/components/logout-other-sessions/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/components/user-profile-form-fields/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/services/login-resource-injector/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/tokens/make-user-confirm-password/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/login-idp-link-confirm-override/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/login-recovery-authn-code-config/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/login-recovery-authn-code-input/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/login-oauth2-device-verify-user-code/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/pages/login-passkeys-conditional-authenticate/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/login/services/i18n/i18n.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ providedIn: 'root' }) 4 | /** INTERNAL: DO NOT IMPORT THIS */ 5 | export class I18nService { 6 | i18n!: unknown; 7 | } 8 | -------------------------------------------------------------------------------- /src/account/services/i18n/i18n.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ providedIn: 'root' }) 4 | /** INTERNAL: DO NOT IMPORT THIS */ 5 | export class I18nService { 6 | i18n!: unknown; 7 | } 8 | -------------------------------------------------------------------------------- /src/login/components/add-remove-buttons-multi-valued-attribute/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../dist", 4 | "lib": { 5 | "entryFile": "index.ts" 6 | }, 7 | "allowedNonPeerDependencies": ["tsafe"] 8 | } 9 | -------------------------------------------------------------------------------- /stories/kc.gen.ts: -------------------------------------------------------------------------------- 1 | export const themeNames = ['keycloakify'] as const; 2 | 3 | export type ThemeName = (typeof themeNames)[number]; 4 | 5 | export type KcEnvName = never; 6 | 7 | export const kcEnvDefaults: Record = {}; 8 | -------------------------------------------------------------------------------- /src/login/tokens/make-user-confirm-password/make-user-confirm-password.token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | export const DO_MAKE_USER_CONFIRM_PASSWORD = new InjectionToken( 4 | 'doMakeUserConfirmPassword' 5 | ); 6 | -------------------------------------------------------------------------------- /src/login/tokens/kc-context/kc-context.token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import type { KcContext } from '@keycloakify/angular/login/KcContext'; 3 | 4 | export const KC_LOGIN_CONTEXT = new InjectionToken('keycloak login context'); 5 | -------------------------------------------------------------------------------- /src/login/tokens/classes/classes.token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import type { ClassKey } from 'keycloakify/login/lib/kcClsx'; 3 | 4 | export const LOGIN_CLASSES = new InjectionToken<{ [key in ClassKey]?: string }>( 5 | 'login classes' 6 | ); 7 | -------------------------------------------------------------------------------- /src/account/tokens/classes/classes.token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import type { ClassKey } from 'keycloakify/account/lib/kcClsx'; 3 | 4 | export const ACCOUNT_CLASSES = new InjectionToken<{ [key in ClassKey]?: string }>( 5 | 'account classes' 6 | ); 7 | -------------------------------------------------------------------------------- /src/account/tokens/kc-context/kc-context.token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import type { KcContext } from '@keycloakify/angular/account/KcContext'; 3 | 4 | export const KC_ACCOUNT_CONTEXT = new InjectionToken( 5 | 'keycloak account context' 6 | ); 7 | -------------------------------------------------------------------------------- /src/login/classes/component-reference/component-reference.class.ts: -------------------------------------------------------------------------------- 1 | import type { ClassKey } from 'keycloakify/login/lib/kcClsx'; 2 | export abstract class ComponentReference { 3 | doUseDefaultCss: boolean | undefined; 4 | classes: Partial> | undefined; 5 | } 6 | -------------------------------------------------------------------------------- /src/bin/tools/kebabCaseToSnakeCase.ts: -------------------------------------------------------------------------------- 1 | import { capitalize } from 'tsafe/capitalize'; 2 | 3 | export function kebabCaseToCamelCase(kebabCaseString: string): string { 4 | const [first, ...rest] = kebabCaseString.split('-'); 5 | 6 | return [first, ...rest.map(capitalize)].join(''); 7 | } 8 | -------------------------------------------------------------------------------- /scripts/shared/run.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from 'child_process'; 2 | import chalk from 'chalk'; 3 | 4 | export function run(command: string, options?: { cwd: string }) { 5 | console.log(chalk.grey(`$ ${command}`)); 6 | 7 | child_process.execSync(command, { stdio: 'inherit', ...options }); 8 | } 9 | -------------------------------------------------------------------------------- /src/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/login/i18n/i18n.ts: -------------------------------------------------------------------------------- 1 | import type { GenericI18n_noJsx } from 'keycloakify/login/i18n/noJsx/GenericI18n_noJsx'; 2 | import type { MessageKey as MessageKey_defaultSet } from 'keycloakify/login/i18n/messages_defaultSet/types'; 3 | /** INTERNAL: DO NOT IMPORT THIS */ 4 | export type I18n = GenericI18n_noJsx; 5 | -------------------------------------------------------------------------------- /src/account/i18n/i18n.ts: -------------------------------------------------------------------------------- 1 | import type { GenericI18n_noJsx } from 'keycloakify/account/i18n/noJsx/GenericI18n_noJsx'; 2 | import type { MessageKey as MessageKey_defaultSet } from 'keycloakify/account/i18n/messages_defaultSet/types'; 3 | /** INTERNAL: DO NOT IMPORT THIS */ 4 | export type I18n = GenericI18n_noJsx; 5 | -------------------------------------------------------------------------------- /src/account/classes/component-reference/component-reference.class.ts: -------------------------------------------------------------------------------- 1 | import { type ClassKey } from 'keycloakify/account'; 2 | export abstract class ComponentReference { 3 | doUseDefaultCss!: boolean | undefined; 4 | classes!: Partial> | undefined; 5 | additionalClasses!: Partial>; 6 | } 7 | -------------------------------------------------------------------------------- /scripts/tools/fs.existsAsync.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs/promises'; 2 | 3 | export async function existsAsync(path: string) { 4 | try { 5 | await fs.stat(path); 6 | return true; 7 | } catch (error) { 8 | if ((error as Error & { code: string }).code === 'ENOENT') return false; 9 | throw error; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /dist/ 3 | /CHANGELOG.md 4 | /.yarn_home/ 5 | /src/test/apps/ 6 | /src/tools/types/ 7 | /build_keycloak/ 8 | /.vscode/ 9 | /src/login/i18n/messages_defaultSet/ 10 | /src/account/i18n/messages_defaultSet/ 11 | /dist_test 12 | /sample_react_project/ 13 | /sample_custom_react_project/ 14 | /keycloakify_starter_test/ 15 | /.storybook/static/keycloak-resources/ -------------------------------------------------------------------------------- /src/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [] 10 | }, 11 | "exclude": ["**/*.spec.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/pipes/to-number/to-number.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, type PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'toNumber' 5 | }) 6 | export class ToNumberPipe implements PipeTransform { 7 | transform(value: string | number): number { 8 | const number = parseInt(`${value}`); 9 | if (isNaN(number)) throw new Error('number is NaN'); 10 | return number; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/pipes/input-type/input-type.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, type PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'inputType' 5 | }) 6 | export class InputTypePipe implements PipeTransform { 7 | transform(inputType?: string): string { 8 | if (inputType?.startsWith('html5-')) { 9 | return inputType.slice(6); 10 | } 11 | 12 | return inputType ?? 'text'; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/bin/initialize-account-theme/boilerplate/i18n.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { i18nBuilder } from '@keycloakify/angular/account'; 3 | import type { ThemeName } from '../kc.gen'; 4 | 5 | /** @see: https://docs.keycloakify.dev/features/i18n */ 6 | const { getI18n, ofTypeI18n } = i18nBuilder.withThemeName().build(); 7 | 8 | type I18n = typeof ofTypeI18n; 9 | 10 | export { getI18n, type I18n }; 11 | -------------------------------------------------------------------------------- /src/bin/core.ts: -------------------------------------------------------------------------------- 1 | export type { BuildContext } from 'keycloakify/bin/shared/buildContext'; 2 | export { 3 | ACCOUNT_THEME_PAGE_IDS, 4 | LOGIN_THEME_PAGE_IDS, 5 | THEME_TYPES, 6 | type AccountThemePageId, 7 | type LoginThemePageId, 8 | type ThemeType 9 | } from 'keycloakify/bin/shared/constants'; 10 | export { 11 | BIN_NAME, 12 | NOT_IMPLEMENTED_EXIT_CODE, 13 | readParams 14 | } from 'keycloakify/bin/shared/customHandler'; 15 | -------------------------------------------------------------------------------- /src/account/account.ts: -------------------------------------------------------------------------------- 1 | import type { Type } from '@angular/core'; 2 | import type { ClassKey } from 'keycloakify/account'; 3 | 4 | export { i18nBuilder } from 'keycloakify/account/i18n/noJsx'; 5 | 6 | export { getDefaultPageComponent } from './defaultPage'; 7 | 8 | export type KcPage = { 9 | PageComponent: Type; 10 | TemplateComponent: Type; 11 | doUseDefaultCss: boolean; 12 | classes: { [key in ClassKey]?: string }; 13 | }; 14 | -------------------------------------------------------------------------------- /stories/account/KcContext.ts: -------------------------------------------------------------------------------- 1 | import type { ExtendKcContext } from 'keycloakify/account'; 2 | import type { KcEnvName, ThemeName } from '../kc.gen'; 3 | 4 | export type KcContextExtension = { 5 | themeName: ThemeName; 6 | properties: Record & {}; 7 | }; 8 | 9 | export type KcContextExtensionPerPage = Record>; 10 | 11 | export type KcContext = ExtendKcContext; 12 | -------------------------------------------------------------------------------- /stories/login/KcContext.ts: -------------------------------------------------------------------------------- 1 | import type { ExtendKcContext } from 'keycloakify/login'; 2 | import type { KcEnvName, ThemeName } from '../kc.gen'; 3 | 4 | export type KcContextExtension = { 5 | themeName: ThemeName; 6 | properties: Record & {}; 7 | }; 8 | 9 | export type KcContextExtensionPerPage = Record>; 10 | 11 | export type KcContext = ExtendKcContext; 12 | -------------------------------------------------------------------------------- /src/bin/initialize-account-theme/boilerplate/KcContext.ts: -------------------------------------------------------------------------------- 1 | import type { ExtendKcContext } from 'keycloakify/account'; 2 | import type { KcEnvName, ThemeName } from '../kc.gen'; 3 | 4 | export type KcContextExtension = { 5 | themeName: ThemeName; 6 | properties: Record & {}; 7 | }; 8 | 9 | export type KcContextExtensionPerPage = Record>; 10 | 11 | export type KcContext = ExtendKcContext; 12 | -------------------------------------------------------------------------------- /src/lib/pipes/to-array/to-array.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, type PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'toArray' 5 | }) 6 | export class ToArrayPipe implements PipeTransform { 7 | transform(value: string | string[], emptyWhenString = false): string[] { 8 | // if (!value) throw new Error('must pass a value'); 9 | if (value instanceof Array) { 10 | return value; 11 | } 12 | return emptyWhenString ? [] : [value]; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@keycloakify/angular", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "peerDependencies": { 6 | "keycloakify": "^11.13.0", 7 | "@angular/core": "^21.0.0", 8 | "@angular/common": "^21.0.0", 9 | "@angular/platform-browser": "^21.0.0" 10 | }, 11 | "dependencies": { 12 | "tsafe": "^1.8.5" 13 | }, 14 | "sideEffects": false, 15 | "publishConfig": { 16 | "access": "public" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /stories/login/pages/saml-post-form.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/saml-post-form.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { 9 | pageId: 'saml-post-form.ftl' 10 | } 11 | }; 12 | 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | export const Default: Story = {}; 18 | -------------------------------------------------------------------------------- /stories/login/i18n.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeName } from '../kc.gen'; 2 | import { i18nBuilder } from '../../src/login'; 3 | 4 | const { getI18n, ofTypeI18n } = i18nBuilder 5 | .withThemeName() 6 | .withExtraLanguages({}) // See: https://docs.keycloakify.dev/i18n/adding-support-for-extra-languages 7 | .withCustomTranslations({}) // See: https://docs.keycloakify.dev/i18n/adding-new-translation-messages-or-changing-the-default-ones 8 | .build(); 9 | type I18n = typeof ofTypeI18n; 10 | export { getI18n, type I18n }; 11 | -------------------------------------------------------------------------------- /stories/account/i18n.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeName } from '../kc.gen'; 2 | import { i18nBuilder } from '../../src/account'; 3 | 4 | const { getI18n, ofTypeI18n } = i18nBuilder 5 | .withThemeName() 6 | .withExtraLanguages({}) // See: https://docs.keycloakify.dev/i18n/adding-support-for-extra-languages 7 | .withCustomTranslations({}) // See: https://docs.keycloakify.dev/i18n/adding-new-translation-messages-or-changing-the-default-ones 8 | .build(); 9 | type I18n = typeof ofTypeI18n; 10 | export { getI18n, type I18n }; 11 | -------------------------------------------------------------------------------- /src/login/login.ts: -------------------------------------------------------------------------------- 1 | import type { Type } from '@angular/core'; 2 | import type { ClassKey } from 'keycloakify/login'; 3 | 4 | export { i18nBuilder } from 'keycloakify/login/i18n/noJsx'; 5 | 6 | export { getDefaultPageComponent } from './defaultPage'; 7 | 8 | export type KcPage = { 9 | PageComponent: Type; 10 | TemplateComponent: Type; 11 | doUseDefaultCss: boolean; 12 | classes: { [key in ClassKey]?: string }; 13 | UserProfileFormFieldsComponent: Type; 14 | doMakeUserConfirmPassword: boolean; 15 | }; 16 | -------------------------------------------------------------------------------- /src/lib/pipes/is-array-with-empty-object/is-array-with-empty-object.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, type PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'isArrayWithEmptyObject' 5 | }) 6 | export class IsArrayWithEmptyObjectPipe implements PipeTransform { 7 | transform(variable: unknown): boolean { 8 | return ( 9 | Array.isArray(variable) && 10 | variable.length === 1 && 11 | typeof variable[0] === 'object' && 12 | Object.keys(variable[0]).length === 0 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /stories/login/pages/login-oauth2-device-verify-user-code.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/login-oauth2-device-verify-user-code.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { pageId: 'login-oauth2-device-verify-user-code.ftl' } 9 | }; 10 | 11 | export default meta; 12 | 13 | type Story = StoryObj; 14 | 15 | export const Default: Story = {}; 16 | -------------------------------------------------------------------------------- /stories/login/pages/login-recovery-authn-code-input.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/login-recovery-authn-code-input.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { 9 | pageId: 'login-recovery-authn-code-input.ftl' 10 | } 11 | }; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const Default: Story = {}; 17 | -------------------------------------------------------------------------------- /stories/login/pages/login-idp-link-confirm-override.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/login-idp-link-confirm-override.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { 9 | pageId: 'login-idp-link-confirm-override.ftl' 10 | } 11 | }; 12 | 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | export const Default: Story = {}; 18 | -------------------------------------------------------------------------------- /stories/login/pages/login-passkeys-conditional-authenticate.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/login-passkeys-conditional-authenticate.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { 9 | pageId: 'login-passkeys-conditional-authenticate.ftl' 10 | } 11 | }; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const Default: Story = {}; 17 | -------------------------------------------------------------------------------- /src/login/components/field-errors/field-errors.component.html: -------------------------------------------------------------------------------- 1 | @let index = fieldIndex(); 2 | 7 | @for (error of displayableErrors(); track error; let i = $index) { 8 | @if (error.fieldIndex === index) { 9 | 10 | @if (displayableErrors()?.length ?? 0 - 1 !== i) { 11 |
12 | } 13 | } 14 | } 15 |
16 | -------------------------------------------------------------------------------- /src/login/components/password-wrapper/password-wrapper.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 15 |
16 | -------------------------------------------------------------------------------- /stories/account/KcContextMock.ts: -------------------------------------------------------------------------------- 1 | import { createGetKcContextMock } from 'keycloakify/account/KcContext'; 2 | import { kcEnvDefaults, themeNames } from '../kc.gen'; 3 | import type { KcContextExtension, KcContextExtensionPerPage } from './KcContext'; 4 | 5 | const kcContextExtension: KcContextExtension = { 6 | themeName: themeNames[0], 7 | properties: { 8 | ...kcEnvDefaults 9 | } 10 | }; 11 | const kcContextExtensionPerPage: KcContextExtensionPerPage = {}; 12 | export const { getKcContextMock } = createGetKcContextMock({ 13 | kcContextExtension, 14 | kcContextExtensionPerPage, 15 | overrides: {}, 16 | overridesPerPage: {} 17 | }); 18 | -------------------------------------------------------------------------------- /stories/login/KcContextMock.ts: -------------------------------------------------------------------------------- 1 | import { createGetKcContextMock } from 'keycloakify/login/KcContext'; 2 | import { kcEnvDefaults, themeNames } from '../kc.gen'; 3 | import type { KcContextExtension, KcContextExtensionPerPage } from './KcContext'; 4 | 5 | const kcContextExtension: KcContextExtension = { 6 | themeName: themeNames[0], 7 | properties: { 8 | ...kcEnvDefaults 9 | } 10 | }; 11 | const kcContextExtensionPerPage: KcContextExtensionPerPage = {}; 12 | export const { getKcContextMock } = createGetKcContextMock({ 13 | kcContextExtension, 14 | kcContextExtensionPerPage, 15 | overrides: {}, 16 | overridesPerPage: {} 17 | }); 18 | -------------------------------------------------------------------------------- /src/login/pages/login-verify-email/login-verify-email.component.html: -------------------------------------------------------------------------------- 1 | @let user = kcContext.user; 2 | 3 | 4 | {{ i18n.msgStr('emailVerifyTitle') }} 5 | 6 | 7 | @let url = kcContext.url; 8 |

9 | {{ i18n.msgStr('emailVerifyInstruction2') }} 10 |
11 | {{ i18n.msgStr('doClickHere') }} 12 |   13 | {{ i18n.msgStr('emailVerifyInstruction3') }} 14 |

15 |
16 | 17 |

18 | {{ i18n.msgStr('emailVerifyInstruction1', user?.email ?? '') }} 19 |

20 | -------------------------------------------------------------------------------- /src/login/pages/code/code.component.html: -------------------------------------------------------------------------------- 1 | @let code = kcContext.code; 2 | 3 | 4 | @let code = kcContext.code; 5 | @if (code.success) { 6 | {{ i18n.msgStr('codeSuccessTitle') }} 7 | } @else { 8 | {{ i18n.msgStr('codeErrorTitle', code.error) }} 9 | } 10 | 11 | 12 |
13 | @if (code.success) { 14 |

{{ i18n.msgStr('copyCodeInstruction') }}

15 | 20 | } @else { 21 |

{{ code.error }}

22 | } 23 |
24 | -------------------------------------------------------------------------------- /src/login/components/logout-other-sessions/logout-other-sessions.component.html: -------------------------------------------------------------------------------- 1 |
5 |
6 |
7 | 17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /src/login/pages/login-page-expired/login-page-expired.component.html: -------------------------------------------------------------------------------- 1 | @let url = kcContext.url; 2 | 3 | 4 | {{ i18n.msgStr('pageExpiredTitle') }} 5 | 6 | 7 |

11 | {{ i18n.msgStr('pageExpiredMsg1') }} 12 | 16 | {{ i18n.msgStr('doClickHere') }} 17 | 18 | .
19 | {{ i18n.msgStr('pageExpiredMsg2') }} 20 | 24 | {{ i18n.msgStr('doClickHere') }} 25 | 26 | . 27 |

28 | -------------------------------------------------------------------------------- /scripts/shared/cleanup.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { transformCodebase } from '../../src/bin/tools/transformCodebase'; 3 | 4 | export function cleanup(params: { distDirPath: string }) { 5 | const { distDirPath } = params; 6 | 7 | if (!fs.existsSync(distDirPath)) { 8 | return; 9 | } 10 | 11 | transformCodebase({ 12 | srcDirPath: distDirPath, 13 | destDirPath: distDirPath, 14 | transformSourceCode: ({ fileRelativePath, sourceCode }) => { 15 | if (fileRelativePath === 'package.json') { 16 | return { modifiedSourceCode: sourceCode }; 17 | } 18 | 19 | return undefined; 20 | } 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/login/pages/error/error.component.html: -------------------------------------------------------------------------------- 1 | @let message = kcContext.message; 2 | @let skipLink = kcContext.skipLink; 3 | @let client = kcContext.client; 4 | 5 | {{ i18n.msgStr('errorTitle') }} 6 | 7 | 8 |
9 |

13 | @if (!skipLink && !!client?.baseUrl) { 14 |

15 | 20 |

21 | } 22 |
23 | -------------------------------------------------------------------------------- /src/bin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": false, 4 | "newLine": "LF", 5 | "noUnusedLocals": true, 6 | "noUnusedParameters": true, 7 | "strict": true, 8 | "downlevelIteration": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "composite": true, 11 | "rootDir": ".", 12 | "module": "ES2020", 13 | "target": "ES2017", 14 | "esModuleInterop": true, 15 | "lib": ["es2015", "ES2019.Object"], 16 | "moduleResolution": "node" 17 | }, 18 | "include": ["**/*.ts"], 19 | "exclude": ["initialize-account-theme/boilerplate"], 20 | "ts-node": { 21 | "esm": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /scripts/build.ts: -------------------------------------------------------------------------------- 1 | import { join as pathJoin } from 'path'; 2 | import chalk from 'chalk'; 3 | import { getThisCodebaseRootDirPath } from './tools/getThisCodebaseRootDirPath'; 4 | import { postNgBuild } from './shared/postNgBuild'; 5 | import { cleanup } from './shared/cleanup'; 6 | import { run } from './shared/run'; 7 | 8 | console.log(chalk.cyan(`Building @keycloakify/angular...`)); 9 | 10 | const startTime = Date.now(); 11 | 12 | const distDirPath = pathJoin(getThisCodebaseRootDirPath(), 'dist'); 13 | 14 | cleanup({ distDirPath }); 15 | 16 | run('npx ng build', { cwd: getThisCodebaseRootDirPath() }); 17 | 18 | postNgBuild(); 19 | 20 | console.log(chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`)); 21 | -------------------------------------------------------------------------------- /stories/login/pages/frontchannel-logout.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/frontchannel-logout.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { 9 | pageId: 'frontchannel-logout.ftl' 10 | } 11 | }; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const Default: Story = {}; 17 | 18 | export const WithoutRedirectUrl: Story = { 19 | globals: { 20 | kcContext: { 21 | logout: { 22 | clients: [] 23 | } 24 | } 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /scripts/tools/getThisCodebaseRootDirPath.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as url from 'url'; 4 | 5 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); 6 | 7 | function getThisCodebaseRootDirPath_rec(dirPath: string): string { 8 | if (fs.existsSync(path.join(dirPath, 'package.json'))) { 9 | return dirPath; 10 | } 11 | return getThisCodebaseRootDirPath_rec(path.join(dirPath, '..')); 12 | } 13 | 14 | let result: string | undefined = undefined; 15 | 16 | export function getThisCodebaseRootDirPath(): string { 17 | if (result !== undefined) { 18 | return result; 19 | } 20 | 21 | return (result = getThisCodebaseRootDirPath_rec(__dirname)); 22 | } 23 | -------------------------------------------------------------------------------- /stories/account/KcPage.ts: -------------------------------------------------------------------------------- 1 | import type { ClassKey } from 'keycloakify/account'; 2 | import { getDefaultPageComponent, type KcPage } from '../../src/account'; 3 | import { TemplateComponent } from '../../src/login/template'; 4 | import type { KcContext } from './KcContext'; 5 | 6 | const classes = {} satisfies { [key in ClassKey]?: string }; 7 | const doUseDefaultCss = true; 8 | export async function getKcPage(pageId: KcContext['pageId']): Promise { 9 | switch (pageId) { 10 | default: 11 | return { 12 | PageComponent: await getDefaultPageComponent(pageId), 13 | TemplateComponent, 14 | doUseDefaultCss, 15 | classes 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/login/components/add-remove-buttons-multi-valued-attribute/add-remove-buttons-multi-valued-attribute.component.html: -------------------------------------------------------------------------------- 1 | @let idPostfix = attribute()?.name ?? '' + '-' + (fieldIndex() ?? 0 + 1); 2 | @if (hasRemove()) { 3 | 11 | @if (hasAdd()) { 12 |  |  13 | } 14 | } 15 | @if (hasAdd()) { 16 | 24 | } 25 | -------------------------------------------------------------------------------- /stories/login/pages/login-username.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/login-username.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { 9 | pageId: 'login-username.ftl' 10 | } 11 | }; 12 | 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | export const Default: Story = {}; 18 | 19 | export const WithEmailAsUsername: Story = { 20 | globals: { 21 | kcContext: { 22 | realm: { 23 | loginWithEmailAllowed: true, 24 | registrationEmailAsUsername: true 25 | } 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/bin/initialize-account-theme/boilerplate/KcPage.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultPageComponent, type KcPage } from '@keycloakify/angular/account'; 2 | import { TemplateComponent } from '@keycloakify/angular/account/template'; 3 | import type { ClassKey } from 'keycloakify/account'; 4 | import type { KcContext } from './KcContext'; 5 | 6 | const classes = {} satisfies { [key in ClassKey]?: string }; 7 | const doUseDefaultCss = true; 8 | 9 | export async function getKcPage(pageId: KcContext['pageId']): Promise { 10 | switch (pageId) { 11 | default: 12 | return { 13 | PageComponent: await getDefaultPageComponent(pageId), 14 | TemplateComponent, 15 | doUseDefaultCss, 16 | classes 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://prettier.io/docs/en/configuration.html 3 | * @type {import("prettier").Config} 4 | */ 5 | const config = { 6 | printWidth: 90, 7 | singleQuote: true, 8 | trailingComma: 'none', 9 | tabWidth: 4, 10 | useTabs: false, 11 | semi: true, 12 | bracketSpacing: true, 13 | arrowParens: 'avoid', 14 | singleAttributePerLine: true, 15 | overrides: [ 16 | { 17 | files: '*.component.ts', 18 | options: { 19 | printWidth: 150 20 | } 21 | }, 22 | { 23 | files: '*.html', 24 | options: { 25 | parser: 'angular', 26 | printWidth: 150 27 | } 28 | } 29 | ] 30 | }; 31 | 32 | export default config; 33 | -------------------------------------------------------------------------------- /src/login/components/textarea-tag/textarea-tag.component.html: -------------------------------------------------------------------------------- 1 | @let attr = attribute(); 2 | @if (attr) { 3 | 16 | } 17 | -------------------------------------------------------------------------------- /src/login/pages/terms/terms.component.html: -------------------------------------------------------------------------------- 1 | @let url = kcContext.url; 2 | 3 | 4 | {{ i18n.msgStr('termsTitle') }} 5 | 6 |
{{ i18n.msgStr('termsText') }}
7 |
12 | 19 | 26 |
27 |
28 | -------------------------------------------------------------------------------- /stories/login/pages/update-email.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/update-email.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { 9 | pageId: 'update-email.ftl' 10 | } 11 | }; 12 | 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | export const Default: Story = {}; 18 | 19 | export const WithAppInitiatedAction: Story = { 20 | globals: { 21 | kcContext: { 22 | url: { 23 | loginAction: '/mock-login-action' 24 | }, 25 | messagesPerField: { 26 | exists: () => false 27 | }, 28 | isAppInitiatedAction: true 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/login/pages/frontchannel-logout/frontchannel-logout.component.html: -------------------------------------------------------------------------------- 1 | @let logout = kcContext.logout; 2 | 3 | 4 | {{ i18n.msgStr('frontchannel-logout.title') }} 5 | 6 |

{{ i18n.msgStr('frontchannel-logout.message') }}

7 |
    8 | @for (client of logout.clients; track client.name) { 9 |
  • 10 | {{ client.name }} 11 | 16 |
  • 17 | } 18 |
19 | @if (logout.logoutRedirectUri !== undefined) { 20 | 25 | {{ i18n.msgStr('doContinue') }} 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/directives/attributes/attributes.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, effect, ElementRef, inject, input, Renderer2 } from '@angular/core'; 2 | 3 | @Directive({ selector: '[kcAttributes]' }) 4 | export class AttributesDirective { 5 | kcAttributes = input>(); 6 | readonly #el = inject>(ElementRef); 7 | readonly #renderer = inject(Renderer2); 8 | 9 | constructor() { 10 | effect(() => { 11 | const attributes = this.kcAttributes(); 12 | if (attributes) { 13 | Object.entries(attributes).forEach(([key, value]) => { 14 | this.#renderer.setAttribute( 15 | this.#el.nativeElement, 16 | `data-${key}`, 17 | value 18 | ); 19 | }); 20 | } 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /scripts/tools/removeNodeModules.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { crawl } from '../../src/bin/tools/crawl'; 3 | 4 | export function removeNodeModules(params: { nodeModulesDirPath: string }) { 5 | const { nodeModulesDirPath } = params; 6 | 7 | try { 8 | fs.rmSync(nodeModulesDirPath, { recursive: true, force: true }); 9 | } catch { 10 | // NOTE: This is a workaround for windows 11 | // we can't remove locked executables. 12 | 13 | crawl({ 14 | dirPath: nodeModulesDirPath, 15 | returnedPathsType: 'absolute' 16 | }).forEach(filePath => { 17 | try { 18 | fs.rmSync(filePath, { force: true }); 19 | } catch (error) { 20 | if (filePath.endsWith('.exe')) { 21 | return; 22 | } 23 | throw error; 24 | } 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /stories/login/pages/login-page-expired.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/login-page-expired.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { 9 | pageId: 'login-page-expired.ftl' 10 | } 11 | }; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const Default: Story = {}; 17 | 18 | export const WithErrorMessage: Story = { 19 | globals: { 20 | kcContext: { 21 | url: { 22 | loginRestartFlowUrl: '/mock-restart-flow', 23 | loginAction: '/mock-continue-login' 24 | }, 25 | message: { 26 | type: 'error', 27 | summary: 'An error occurred while processing your session.' 28 | } 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /stories/login/pages/login-update-profile.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/login-update-profile.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { 9 | pageId: 'login-update-profile.ftl' 10 | } 11 | }; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const Default: Story = {}; 17 | 18 | export const WithProfileError: Story = { 19 | globals: { 20 | kcContext: { 21 | url: { 22 | loginAction: '/mock-login-action' 23 | }, 24 | messagesPerField: { 25 | existsError: (field: string) => field === 'email', 26 | get: () => 'Invalid email format' 27 | }, 28 | isAppInitiatedAction: false 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | .vscode 40 | 41 | .DS_Store 42 | 43 | /dist 44 | /.yarn_home/ 45 | 46 | .idea 47 | 48 | # VS Code devcontainers 49 | .devcontainer 50 | /.yarn 51 | /.yarnrc.yml 52 | 53 | .angular -------------------------------------------------------------------------------- /stories/login/KcPage.ts: -------------------------------------------------------------------------------- 1 | import type { ClassKey } from 'keycloakify/login'; 2 | import { getDefaultPageComponent, type KcPage } from '../../src/login'; 3 | import { UserProfileFormFieldsComponent } from '../../src/login/components/user-profile-form-fields'; 4 | import { TemplateComponent } from '../../src/login/template'; 5 | import type { KcContext } from './KcContext'; 6 | 7 | const classes = {} satisfies { [key in ClassKey]?: string }; 8 | const doUseDefaultCss = true; 9 | const doMakeUserConfirmPassword = true; 10 | export async function getKcPage(pageId: KcContext['pageId']): Promise { 11 | switch (pageId) { 12 | default: 13 | return { 14 | PageComponent: await getDefaultPageComponent(pageId), 15 | TemplateComponent, 16 | UserProfileFormFieldsComponent, 17 | doMakeUserConfirmPassword, 18 | doUseDefaultCss, 19 | classes 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/login/pages/login-idp-link-confirm-override/login-idp-link-confirm-override.component.html: -------------------------------------------------------------------------------- 1 | @let url = kcContext.url; 2 | @let idpDisplayName = kcContext.idpDisplayName; 3 | 4 | 5 | {{ i18n.msgStr('confirmOverrideIdpTitle') }} 6 | 7 |
12 | {{ i18n.msgStr('pageExpiredMsg1') }} 13 | 17 | {{ i18n.msgStr('doClickHere') }} 18 | 19 |
20 |
21 | 30 |
31 | -------------------------------------------------------------------------------- /src/bin/tools/getThisCodebaseRootDirPath.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as url from 'url'; 4 | 5 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); 6 | 7 | function getThisCodebaseRootDirPath_rec(dirPath: string): string { 8 | if (fs.existsSync(path.join(dirPath, 'LICENSE'))) { 9 | return dirPath; 10 | } 11 | return getThisCodebaseRootDirPath_rec(path.join(dirPath, '..')); 12 | } 13 | 14 | let result: string | undefined = undefined; 15 | 16 | export function getThisCodebaseRootDirPath(): string { 17 | if (result !== undefined) { 18 | return result; 19 | } 20 | 21 | return (result = getThisCodebaseRootDirPath_rec(__dirname)); 22 | } 23 | 24 | export function getNearestPackageJsonDirPath(dirPath: string): string { 25 | if (fs.existsSync(path.join(dirPath, 'package.json'))) { 26 | return dirPath; 27 | } 28 | return getNearestPackageJsonDirPath(path.join(dirPath, '..')); 29 | } 30 | -------------------------------------------------------------------------------- /stories/login/pages/login-recovery-authn-code-config.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/login-recovery-authn-code-config.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { 9 | pageId: 'login-recovery-authn-code-config.ftl' 10 | } 11 | }; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const Default: Story = {}; 17 | 18 | export const WithErrorDuringCodeGeneration: Story = { 19 | globals: { 20 | kcContext: { 21 | url: { 22 | loginAction: '/mock-login-action' 23 | }, 24 | message: { 25 | summary: 26 | 'An error occurred during recovery code generation. Please try again.', 27 | type: 'error' 28 | } 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/login/pages/login-idp-link-email/login-idp-link-email.component.html: -------------------------------------------------------------------------------- 1 | @let idpAlias = kcContext.idpAlias; 2 | @let brokerContext = kcContext.brokerContext; 3 | @let realm = kcContext.realm; 4 | @let url = kcContext.url; 5 | 6 | 7 | @let idpAlias = kcContext.idpAlias; 8 | {{ i18n.msgStr('emailLinkIdpTitle', idpAlias) }} 9 | 10 |

14 | {{ i18n.msgStr('emailLinkIdp1', idpAlias, brokerContext.username, realm.displayName) }} 15 |

16 |

20 | {{ i18n.msgStr('emailLinkIdp2') }} 21 | {{ i18n.msgStr('doClickHere') }} 22 | {{ i18n.msgStr('emailLinkIdp3') }} 23 |

24 |

28 | {{ i18n.msgStr('emailLinkIdp4') }} 29 | {{ i18n.msgStr('doClickHere') }} 30 | {{ i18n.msgStr('emailLinkIdp5') }} 31 |

32 | -------------------------------------------------------------------------------- /src/login/pages/delete-credential/delete-credential.component.html: -------------------------------------------------------------------------------- 1 | @let url = kcContext.url; 2 | @let credentialLabel = kcContext.credentialLabel; 3 | 4 | @let credentialLabel = kcContext.credentialLabel; 5 | {{ i18n.msgStr('deleteCredentialTitle', credentialLabel) }} 6 | 7 | 8 |
9 | {{ i18n.msgStr('deleteCredentialMessage', credentialLabel) }} 10 |
11 | 12 |
17 | 24 | 25 | 32 |
33 |
34 | -------------------------------------------------------------------------------- /src/bin/tools/fs.rmSync.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { join as pathJoin } from 'path'; 3 | import { SemVer } from './SemVer'; 4 | 5 | /** 6 | * Polyfill of fs.rmSync(dirPath, { "recursive": true }) 7 | * For older version of Node 8 | */ 9 | export function rmSync(dirPath: string, options: { recursive: true; force?: true }) { 10 | if (SemVer.compare(SemVer.parse(process.version), SemVer.parse('14.14.0')) > 0) { 11 | fs.rmSync(dirPath, options); 12 | return; 13 | } 14 | 15 | const { force = true } = options; 16 | 17 | if (force && !fs.existsSync(dirPath)) { 18 | return; 19 | } 20 | 21 | const removeDir_rec = (dirPath: string) => 22 | fs.readdirSync(dirPath).forEach(basename => { 23 | const fileOrDirPath = pathJoin(dirPath, basename); 24 | 25 | if (fs.lstatSync(fileOrDirPath).isDirectory()) { 26 | removeDir_rec(fileOrDirPath); 27 | return; 28 | } else { 29 | fs.unlinkSync(fileOrDirPath); 30 | } 31 | }); 32 | 33 | removeDir_rec(dirPath); 34 | } 35 | -------------------------------------------------------------------------------- /src/login/pages/idp-review-user-profile/idp-review-user-profile.component.html: -------------------------------------------------------------------------------- 1 | @let url = kcContext.url; 2 | 3 | 4 | {{ i18n.msgStr('loginIdpReviewProfileTitle') }} 5 | 6 |
12 | 13 |
14 |
18 |
19 |
20 |
24 | 30 |
31 |
32 | 33 | -------------------------------------------------------------------------------- /src/bin/tools/crawl.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { join as pathJoin, relative as pathRelative } from 'path'; 3 | 4 | const crawlRec = (dirPath: string, filePaths: string[]) => { 5 | for (const basename of fs.readdirSync(dirPath)) { 6 | const fileOrDirPath = pathJoin(dirPath, basename); 7 | 8 | if (fs.lstatSync(fileOrDirPath).isDirectory()) { 9 | crawlRec(fileOrDirPath, filePaths); 10 | 11 | continue; 12 | } 13 | 14 | filePaths.push(fileOrDirPath); 15 | } 16 | }; 17 | 18 | /** List all files in a given directory return paths relative to the dir_path */ 19 | export function crawl(params: { 20 | dirPath: string; 21 | returnedPathsType: 'absolute' | 'relative to dirPath'; 22 | }): string[] { 23 | const { dirPath, returnedPathsType } = params; 24 | 25 | const filePaths: string[] = []; 26 | 27 | crawlRec(dirPath, filePaths); 28 | 29 | switch (returnedPathsType) { 30 | case 'absolute': 31 | return filePaths; 32 | case 'relative to dirPath': 33 | return filePaths.map(filePath => pathRelative(dirPath, filePath)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/login/pages/login-idp-link-confirm/login-idp-link-confirm.component.html: -------------------------------------------------------------------------------- 1 | @let url = kcContext.url; 2 | @let idpAlias = kcContext.idpAlias; 3 | 4 | {{ i18n.msgStr('confirmLinkIdpTitle') }} 5 | 6 |
11 |
12 | 21 | 30 |
31 |
32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 GitHub user u/kathari00 u/LucaPeruzzo u/garronej 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /stories/login/pages/delete-account-confirm.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/delete-account-confirm.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { 9 | pageId: 'delete-account-confirm.ftl' 10 | } 11 | }; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const Default: Story = {}; 17 | 18 | export const WithAIAFlow: Story = { 19 | globals: { 20 | kcContext: { 21 | triggered_from_aia: true, 22 | url: { loginAction: '/login-action' } 23 | } 24 | } 25 | }; 26 | 27 | export const WithoutAIAFlow: Story = { 28 | globals: { 29 | kcContext: { 30 | triggered_from_aia: false, 31 | url: { loginAction: '/login-action' } 32 | } 33 | } 34 | }; 35 | 36 | export const WithCustomButtonStyle: Story = { 37 | globals: { 38 | kcContext: { 39 | triggered_from_aia: true, 40 | url: { loginAction: '/login-action' } 41 | } 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /src/login/components/input-tag-selects/input-tag-selects.component.html: -------------------------------------------------------------------------------- 1 | @let inputType = context()?.inputType; 2 | @let classInput = context()?.classInput; 3 | @let classLabel = context()?.classLabel; 4 | @let classDiv = context()?.classDiv; 5 | @let attr = attribute(); 6 | @if (attr) { 7 | @for (option of options(); track option) { 8 |
9 | 21 | 27 |
28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/login/pages/saml-post-form/saml-post-form.component.html: -------------------------------------------------------------------------------- 1 | @let samlPost = kcContext.samlPost; 2 | 3 | 4 | {{ i18n.msgStr('saml.post-form.title') }} 5 | 6 |

{{ i18n.msgStr('saml.post-form.message') }}

7 |
13 | @if (!!samlPost.SAMLRequest) { 14 | 19 | } 20 | @if (!!samlPost.SAMLResponse) { 21 | 26 | } 27 | @if (!!samlPost.relayState) { 28 | 33 | } 34 | 41 |
42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "paths": { 6 | "@keycloakify/angular/*": ["./src/*"], 7 | "@keycloakify/angular": ["./src"] 8 | }, 9 | "outDir": "./dist/out-tsc", 10 | "strict": true, 11 | "noImplicitOverride": true, 12 | "noPropertyAccessFromIndexSignature": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "skipLibCheck": true, 16 | "esModuleInterop": true, 17 | "sourceMap": true, 18 | "declaration": false, 19 | "experimentalDecorators": true, 20 | "moduleResolution": "bundler", 21 | "importHelpers": true, 22 | "target": "ES2022", 23 | "module": "ES2022", 24 | "useDefineForClassFields": false, 25 | "verbatimModuleSyntax": true 26 | }, 27 | "angularCompilerOptions": { 28 | "enableI18nLegacyMessageIdFormat": false, 29 | "strictInjectionParameters": true, 30 | "strictInputAccessModifiers": true, 31 | "strictTemplates": true 32 | }, 33 | "exclude": ["./src/bin", "./src/stories"] 34 | } 35 | -------------------------------------------------------------------------------- /src/login/components/group-label/group-label.component.html: -------------------------------------------------------------------------------- 1 | @let attr = attribute(); 2 | @let groupName = groupNameRef(); 3 | @if (attr && groupName !== '') { 4 |
8 | @let groupDisplayHeader = attr.group?.displayHeader ?? ''; 9 | @let groupHeaderText = groupDisplayHeader !== '' ? groupDisplayHeader : (attr.group?.name ?? ''); 10 |
11 | 17 |
18 | @let groupDisplayDescription = attr.group?.displayDescription ?? ''; 19 | @if (groupDisplayDescription !== '') { 20 |
21 | 27 |
28 | } 29 |
30 | } 31 | -------------------------------------------------------------------------------- /scripts/tools/Deferred.ts: -------------------------------------------------------------------------------- 1 | import { overwriteReadonlyProp } from 'tsafe/lab/overwriteReadonlyProp'; 2 | 3 | export class Deferred { 4 | public readonly pr: Promise; 5 | 6 | /** NOTE: Does not need to be called bound to instance*/ 7 | public readonly resolve: (value: T) => void; 8 | public readonly reject: (error: any) => void; 9 | 10 | constructor() { 11 | let resolve!: (value: T) => void; 12 | let reject!: (error: any) => void; 13 | 14 | this.pr = new Promise((resolve_, reject_) => { 15 | resolve = value => { 16 | overwriteReadonlyProp(this, 'isPending', false); 17 | resolve_(value); 18 | }; 19 | 20 | reject = error => { 21 | overwriteReadonlyProp(this, 'isPending', false); 22 | reject_(error); 23 | }; 24 | }); 25 | 26 | this.resolve = resolve; 27 | this.reject = reject; 28 | } 29 | 30 | public readonly isPending: boolean = true; 31 | } 32 | 33 | export namespace Deferred { 34 | export type Unpack> = T extends Deferred ? U : never; 35 | } 36 | 37 | export class VoidDeferred extends Deferred { 38 | declare public readonly resolve: () => void; 39 | } 40 | -------------------------------------------------------------------------------- /src/login/components/select-tag/select-tag.component.html: -------------------------------------------------------------------------------- 1 | @let attr = attribute(); 2 | @let multiple = isMultiple(); 3 | @if (attr) { 4 | 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/pipes/kc-sanitize/kc-sanitize.pipe.ts: -------------------------------------------------------------------------------- 1 | import { inject, Pipe, type PipeTransform } from '@angular/core'; 2 | import { 3 | DomSanitizer, 4 | type SafeHtml, 5 | type SafeResourceUrl, 6 | type SafeScript, 7 | type SafeStyle, 8 | type SafeUrl 9 | } from '@angular/platform-browser'; 10 | 11 | @Pipe({ name: 'kcSanitize' }) 12 | export class KcSanitizePipe implements PipeTransform { 13 | #sanitizer = inject(DomSanitizer); 14 | 15 | transform( 16 | value: string, 17 | type: 'html' | 'style' | 'script' | 'url' | 'resourceUrl' 18 | ): SafeHtml | SafeStyle | SafeScript | SafeUrl | SafeResourceUrl { 19 | switch (type) { 20 | case 'html': 21 | return this.#sanitizer.bypassSecurityTrustHtml(value); 22 | case 'style': 23 | return this.#sanitizer.bypassSecurityTrustStyle(value); 24 | case 'script': 25 | return this.#sanitizer.bypassSecurityTrustScript(value); 26 | case 'url': 27 | return this.#sanitizer.bypassSecurityTrustUrl(value); 28 | case 'resourceUrl': 29 | return this.#sanitizer.bypassSecurityTrustResourceUrl(value); 30 | default: 31 | throw new Error(`Invalid safe type specified: ${type}`); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /stories/login/pages/webauthn-register.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/webauthn-register.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { 9 | pageId: 'webauthn-register.ftl' 10 | } 11 | }; 12 | 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | export const Default: Story = {}; 18 | 19 | export const WithRetryAvailable: Story = { 20 | globals: { 21 | kcContext: { 22 | url: { 23 | loginAction: '/mock-login-action' 24 | }, 25 | isSetRetry: true, 26 | isAppInitiatedAction: false 27 | } 28 | } 29 | }; 30 | 31 | export const WithErrorDuringRegistration: Story = { 32 | globals: { 33 | kcContext: { 34 | url: { 35 | loginAction: '/mock-login-action' 36 | }, 37 | isSetRetry: false, 38 | isAppInitiatedAction: false, 39 | message: { 40 | summary: 41 | 'An error occurred during WebAuthn registration. Please try again.', 42 | type: 'error' 43 | } 44 | } 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /stories/login/pages/code.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/code.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { 9 | pageId: 'code.ftl' 10 | } 11 | }; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const Default: Story = {}; 17 | 18 | export const WithErrorCode: Story = { 19 | globals: { 20 | kcContext: { 21 | code: { 22 | success: false, 23 | error: 'Failed to generate code' 24 | } 25 | } 26 | } 27 | }; 28 | export const WithFrenchLanguage: Story = { 29 | globals: { 30 | kcContext: { 31 | locale: { 32 | currentLanguageTag: 'fr' 33 | }, 34 | code: { 35 | success: true, 36 | code: 'XYZ789' 37 | } 38 | } 39 | } 40 | }; 41 | 42 | export const WithHtmlErrorMessage: Story = { 43 | globals: { 44 | kcContext: { 45 | code: { 46 | success: false, 47 | error: "Something went wrong. Try again" 48 | } 49 | } 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /stories/login/pages/login-x509-info.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/login-x509-info.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { 9 | pageId: 'login-x509-info.ftl' 10 | } 11 | }; 12 | 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | export const Default: Story = {}; 18 | 19 | /** 20 | * WithoutUserEnabled: 21 | * - Purpose: Tests when the user is not enabled to log in via x509. 22 | * - Scenario: The component renders the certificate details but does not provide the option to log in or cancel. 23 | * - Key Aspect: Ensures that the login buttons are not displayed when the user is not enabled. 24 | */ 25 | export const WithoutUserEnabled: Story = { 26 | globals: { 27 | kcContext: { 28 | url: { 29 | loginAction: '/mock-login-action' 30 | }, 31 | x509: { 32 | formData: { 33 | subjectDN: 'CN=John Doe, OU=Example Org, O=Example Inc, C=US', 34 | username: 'johndoe', 35 | isUserEnabled: false // User not enabled for login 36 | } 37 | } 38 | } 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /scripts/tools/fs.rm.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs/promises'; 2 | import { join as pathJoin } from 'path'; 3 | import { SemVer } from './SemVer'; 4 | 5 | /** 6 | * Polyfill of fs.rm(dirPath, { "recursive": true }) 7 | * For older version of Node 8 | */ 9 | export async function rm(dirPath: string, options: { recursive: true; force?: true }) { 10 | if (SemVer.compare(SemVer.parse(process.version), SemVer.parse('14.14.0')) > 0) { 11 | return fs.rm(dirPath, options); 12 | } 13 | 14 | const { force = true } = options; 15 | 16 | if (force && !(await checkDirExists(dirPath))) { 17 | return; 18 | } 19 | 20 | const removeDir_rec = async (dirPath: string) => 21 | Promise.all( 22 | (await fs.readdir(dirPath)).map(async basename => { 23 | const fileOrDirpath = pathJoin(dirPath, basename); 24 | 25 | if ((await fs.lstat(fileOrDirpath)).isDirectory()) { 26 | await removeDir_rec(fileOrDirpath); 27 | } else { 28 | await fs.unlink(fileOrDirpath); 29 | } 30 | }) 31 | ); 32 | 33 | await removeDir_rec(dirPath); 34 | } 35 | 36 | async function checkDirExists(dirPath: string) { 37 | try { 38 | await fs.access(dirPath, fs.constants.F_OK); 39 | return true; 40 | } catch { 41 | return false; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/bin/tools/String.prototype.replaceAll.ts: -------------------------------------------------------------------------------- 1 | export function replaceAll( 2 | string: string, 3 | searchValue: string | RegExp, 4 | replaceValue: string 5 | ): string { 6 | if ((string as any).replaceAll !== undefined) { 7 | return (string as any).replaceAll(searchValue, replaceValue); 8 | } 9 | 10 | // If the searchValue is a string 11 | if (typeof searchValue === 'string') { 12 | // Escape special characters in the string to be used in a regex 13 | var escapedSearchValue = searchValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 14 | var regex = new RegExp(escapedSearchValue, 'g'); 15 | 16 | return string.replace(regex, replaceValue); 17 | } 18 | 19 | // If the searchValue is a global RegExp, use it directly 20 | if (searchValue instanceof RegExp && searchValue.global) { 21 | return string.replace(searchValue, replaceValue); 22 | } 23 | 24 | // If the searchValue is a non-global RegExp, throw an error 25 | if (searchValue instanceof RegExp) { 26 | throw new TypeError('replaceAll must be called with a global RegExp'); 27 | } 28 | 29 | // Convert searchValue to string if it's not a string or RegExp 30 | var searchString = String(searchValue); 31 | var regexFromString = new RegExp( 32 | searchString.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 33 | 'g' 34 | ); 35 | 36 | return string.replace(regexFromString, replaceValue); 37 | } 38 | -------------------------------------------------------------------------------- /stories/login/pages/login-update-password.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/login-update-password.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { 9 | pageId: 'login-update-password.ftl' 10 | } 11 | }; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const Default: Story = {}; 17 | 18 | export const WithPasswordError: Story = { 19 | globals: { 20 | kcContext: { 21 | url: { 22 | loginAction: '/mock-login-action' 23 | }, 24 | messagesPerField: { 25 | existsError: (field: string) => field === 'password', 26 | get: () => 'Password must be at least 8 characters long.' 27 | }, 28 | isAppInitiatedAction: false 29 | } 30 | } 31 | }; 32 | 33 | export const WithPasswordConfirmError: Story = { 34 | globals: { 35 | kcContext: { 36 | url: { 37 | loginAction: '/mock-login-action' 38 | }, 39 | messagesPerField: { 40 | existsError: (field: string) => field === 'password-confirm', 41 | get: () => 'Passwords do not match.' 42 | }, 43 | isAppInitiatedAction: false 44 | } 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /stories/login/pages/logout-confirm.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/logout-confirm.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { 9 | pageId: 'logout-confirm.ftl' 10 | } 11 | }; 12 | 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | export const Default: Story = {}; 18 | 19 | /** 20 | * WithCustomLogoutMessage: 21 | * - Purpose: Tests when a custom message is displayed for the logout confirmation. 22 | * - Scenario: The component renders with a custom logout confirmation message instead of the default one. 23 | * - Key Aspect: Ensures the custom logout message is displayed correctly. 24 | */ 25 | export const WithCustomLogoutMessage: Story = { 26 | globals: { 27 | kcContext: { 28 | url: { 29 | logoutConfirmAction: '/mock-logout-action' 30 | }, 31 | client: { 32 | baseUrl: '/mock-client-url' 33 | }, 34 | logoutConfirm: { 35 | code: 'mock-session-code', 36 | skipLink: false 37 | }, 38 | message: { 39 | summary: 'Are you sure you want to log out from all sessions?', 40 | type: 'warning' 41 | } 42 | } 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/login/pages/webauthn-error/webauthn-error.component.html: -------------------------------------------------------------------------------- 1 | @let url = kcContext.url; 2 | @let isAppInitiatedAction = kcContext.isAppInitiatedAction; 3 | 4 | 5 | {{ i18n.msgStr('webauthn-error-title') }} 6 | 7 |
13 | 18 | 23 |
24 | 33 | @if (isAppInitiatedAction) { 34 |
40 | 49 |
50 | } 51 | -------------------------------------------------------------------------------- /src/bin/main.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { readParams, NOT_IMPLEMENTED_EXIT_CODE } from './core'; 4 | import chalk from 'chalk'; 5 | 6 | const { buildContext, commandName } = readParams({ apiVersion: 'v1' }); 7 | 8 | (async () => { 9 | switch (commandName) { 10 | case 'add-story': 11 | { 12 | const { command } = await import('./add-story'); 13 | command({ buildContext }); 14 | } 15 | return; 16 | case 'eject-page': 17 | { 18 | const { command } = await import('./eject-page'); 19 | command({ buildContext }); 20 | } 21 | return; 22 | case 'update-kc-gen': 23 | { 24 | const { command } = await import('./update-kc-gen'); 25 | command({ buildContext }); 26 | } 27 | return; 28 | case 'initialize-account-theme': 29 | { 30 | const { command } = await import('./initialize-account-theme'); 31 | command({ buildContext }); 32 | } 33 | return; 34 | case 'initialize-admin-theme': 35 | { 36 | console.log( 37 | chalk.red('Cannot create an admin theme when using Angular.') 38 | ); 39 | process.exit(1); 40 | } 41 | return; 42 | default: 43 | process.exit(NOT_IMPLEMENTED_EXIT_CODE); 44 | } 45 | })(); 46 | -------------------------------------------------------------------------------- /src/login/components/logout-other-sessions/logout-other-sessions.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, forwardRef, inject } from '@angular/core'; 2 | import { USE_DEFAULT_CSS } from '@keycloakify/angular/lib/tokens/use-default-css'; 3 | import { ComponentReference } from '@keycloakify/angular/login/classes/component-reference'; 4 | import { KcClassDirective } from '@keycloakify/angular/login/directives/kc-class'; 5 | import { LOGIN_CLASSES } from '@keycloakify/angular/login/tokens/classes'; 6 | import { LOGIN_I18N } from '@keycloakify/angular/login/tokens/i18n'; 7 | import type { ClassKey } from 'keycloakify/login/lib/kcClsx'; 8 | import type { I18n } from '@keycloakify/angular/login/i18n'; 9 | 10 | @Component({ 11 | selector: 'kc-logout-other-sessions', 12 | styles: [ 13 | ` 14 | :host { 15 | display: contents; 16 | } 17 | ` 18 | ], 19 | imports: [KcClassDirective], 20 | changeDetection: ChangeDetectionStrategy.OnPush, 21 | templateUrl: 'logout-other-sessions.component.html', 22 | providers: [ 23 | { 24 | provide: ComponentReference, 25 | useExisting: forwardRef(() => LogoutOtherSessionsComponent) 26 | } 27 | ] 28 | }) 29 | export class LogoutOtherSessionsComponent extends ComponentReference { 30 | i18n = inject(LOGIN_I18N); 31 | override doUseDefaultCss = inject(USE_DEFAULT_CSS); 32 | override classes = inject>>(LOGIN_CLASSES); 33 | } 34 | -------------------------------------------------------------------------------- /scripts/tools/waitForThrottle.ts: -------------------------------------------------------------------------------- 1 | import { Deferred } from './Deferred'; 2 | import { createStatefulObservable } from './StatefulObservable'; 3 | 4 | export function createWaitForThrottle(params: { delay: number }) { 5 | const { delay } = params; 6 | 7 | const obsCurr = createStatefulObservable< 8 | { timer: ReturnType; startTime: number } | undefined 9 | >(() => undefined); 10 | 11 | function waitForThrottle(): Promise { 12 | const dOut = new Deferred(); 13 | 14 | const timerCallback = () => { 15 | obsCurr.current = undefined; 16 | dOut.resolve(); 17 | }; 18 | 19 | if (obsCurr.current !== undefined) { 20 | clearTimeout(obsCurr.current.timer); 21 | 22 | obsCurr.current.timer = setTimeout( 23 | timerCallback, 24 | delay - (Date.now() - obsCurr.current.startTime) 25 | ); 26 | 27 | return dOut.pr; 28 | } else { 29 | const startTime = Date.now(); 30 | 31 | obsCurr.current = { 32 | timer: setTimeout(timerCallback, delay), 33 | startTime 34 | }; 35 | } 36 | 37 | return dOut.pr; 38 | } 39 | 40 | const obsIsDebouncing = createStatefulObservable(() => false); 41 | 42 | obsCurr.subscribe(curr => (obsIsDebouncing.current = curr !== undefined)); 43 | 44 | return { 45 | waitForThrottle, 46 | obsIsDebouncing 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/login/pages/info/info.component.html: -------------------------------------------------------------------------------- 1 | @let skipLink = kcContext.skipLink; 2 | @let actionUri = kcContext.actionUri; 3 | @let pageRedirectUri = kcContext.pageRedirectUri; 4 | @let client = kcContext.client; 5 | 6 | 7 | @let messageHeader = kcContext.messageHeader; 8 | @let message = kcContext.message; 9 | 10 | 11 |
12 |

16 | @if (!skipLink) { 17 | @if (pageRedirectUri) { 18 |

19 | 23 | 24 |

25 | } @else if (actionUri) { 26 |

27 | 31 | 32 |

33 | } @else if (client.baseUrl) { 34 |

35 | 39 | 40 |

41 | } 42 | } 43 |
44 | -------------------------------------------------------------------------------- /src/account/pages/federatedIdentity/federatedIdentity.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, forwardRef, inject } from '@angular/core'; 2 | import { ComponentReference } from '@keycloakify/angular/account/classes/component-reference'; 3 | import type { I18n } from '@keycloakify/angular/account/i18n'; 4 | import type { KcContext } from '@keycloakify/angular/account/KcContext'; 5 | import { ACCOUNT_CLASSES } from '@keycloakify/angular/account/tokens/classes'; 6 | import { ACCOUNT_I18N } from '@keycloakify/angular/account/tokens/i18n'; 7 | import { KC_ACCOUNT_CONTEXT } from '@keycloakify/angular/account/tokens/kc-context'; 8 | import { USE_DEFAULT_CSS } from '@keycloakify/angular/lib/tokens/use-default-css'; 9 | import type { ClassKey } from 'keycloakify/account'; 10 | 11 | @Component({ 12 | selector: 'kc-federated-identity', 13 | templateUrl: 'federatedIdentity.component.html', 14 | changeDetection: ChangeDetectionStrategy.OnPush, 15 | providers: [ 16 | { 17 | provide: ComponentReference, 18 | useExisting: forwardRef(() => FederatedIdentityComponent) 19 | } 20 | ] 21 | }) 22 | export class FederatedIdentityComponent extends ComponentReference { 23 | i18n = inject(ACCOUNT_I18N); 24 | kcContext = inject>(KC_ACCOUNT_CONTEXT); 25 | override doUseDefaultCss = inject(USE_DEFAULT_CSS); 26 | override classes = inject>>(ACCOUNT_CLASSES); 27 | active = 'social'; 28 | } 29 | -------------------------------------------------------------------------------- /stories/login/pages/error.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/error.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { 9 | pageId: 'error.ftl' 10 | } 11 | }; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const Default: Story = {}; 17 | 18 | export const WithAnotherMessage: Story = { 19 | globals: { 20 | kcContext: { 21 | message: { summary: 'With another error message' } 22 | } 23 | } 24 | }; 25 | 26 | export const WithHtmlErrorMessage: Story = { 27 | globals: { 28 | kcContext: { 29 | message: { 30 | summary: 31 | "Error: Something went wrong. Go back" 32 | } 33 | } 34 | } 35 | }; 36 | 37 | export const FrenchError: Story = { 38 | globals: { 39 | kcContext: { 40 | locale: { currentLanguageTag: 'fr' }, 41 | message: { summary: "Une erreur s'est produite" } 42 | } 43 | } 44 | }; 45 | 46 | export const WithSkipLink: Story = { 47 | globals: { 48 | kcContext: { 49 | message: { summary: 'An error occurred' }, 50 | skipLink: true, 51 | client: { 52 | baseUrl: 'https://example.com' 53 | } 54 | } 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /stories/login/pages/login-reset-password.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/login-reset-password.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { 9 | pageId: 'login-reset-password.ftl' 10 | } 11 | }; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const Default: Story = {}; 17 | 18 | export const WithEmailAsUsername: Story = { 19 | globals: { 20 | kcContext: { 21 | realm: { 22 | loginWithEmailAllowed: true, 23 | registrationEmailAsUsername: true 24 | } 25 | } 26 | } 27 | }; 28 | 29 | export const WithUsernameError: Story = { 30 | globals: { 31 | kcContext: { 32 | realm: { 33 | loginWithEmailAllowed: false, 34 | registrationEmailAsUsername: false, 35 | duplicateEmailsAllowed: false 36 | }, 37 | url: { 38 | loginAction: '/mock-login-action', 39 | loginUrl: '/mock-login-url' 40 | }, 41 | messagesPerField: { 42 | existsError: (field: string) => field === 'username', 43 | get: () => 'Invalid username' 44 | }, 45 | auth: { 46 | attemptedUsername: 'invalid_user' 47 | } 48 | } 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /stories/login/pages/login-password.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/login-password.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { 9 | pageId: 'login-password.ftl' 10 | } 11 | }; 12 | 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | export const Default: Story = {}; 18 | 19 | export const WithPasswordError: Story = { 20 | globals: { 21 | kcContext: { 22 | realm: { 23 | resetPasswordAllowed: true 24 | }, 25 | url: { 26 | loginAction: '/mock-login', 27 | loginResetCredentialsUrl: '/mock-reset-password' 28 | }, 29 | messagesPerField: { 30 | existsError: (field: string) => field === 'password', 31 | get: () => 'Invalid password' 32 | } 33 | } 34 | } 35 | }; 36 | 37 | export const WithoutResetPasswordOption: Story = { 38 | globals: { 39 | kcContext: { 40 | realm: { 41 | resetPasswordAllowed: false 42 | }, 43 | url: { 44 | loginAction: '/mock-login', 45 | loginResetCredentialsUrl: '/mock-reset-password' 46 | }, 47 | messagesPerField: { 48 | existsError: () => false 49 | } 50 | } 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /src/account/pages/log/log.component.html: -------------------------------------------------------------------------------- 1 | @let log = kcContext.log; 2 | 3 |
4 |
5 |

{{ i18n.msgStr('accountLogHtmlTitle') }}

6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | @for (event of log.events; track event) { 21 | 22 | 23 | 24 | 25 | 26 | 36 | 37 | } 38 | 39 |
{{ i18n.msgStr('date') }}{{ i18n.msgStr('event') }}{{ i18n.msgStr('ip') }}{{ i18n.msgStr('client') }}{{ i18n.msgStr('details') }}
{{ event.date ? (event.date | date) : '' }}{{ event.event }}{{ event.ipAddress }}{{ event.client || '' }} 27 | @for (detail of event.details; track detail; let last = $last) { 28 | 29 | {{ detail.key }} = {{ detail.value }} 30 | @if (!last) { 31 | ,  32 | } 33 | 34 | } 35 |
40 |
41 | -------------------------------------------------------------------------------- /stories/login/pages/delete-credential.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/delete-credential.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { 9 | pageId: 'delete-credential.ftl' 10 | } 11 | }; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const Default: Story = {}; 17 | 18 | export const WithCustomCredentialLabel: Story = { 19 | globals: { 20 | kcContext: { 21 | credentialLabel: 'Test Credential', 22 | url: { loginAction: '/login-action' } 23 | } 24 | } 25 | }; 26 | 27 | export const WithSuccessMessage: Story = { 28 | globals: { 29 | kcContext: { 30 | message: { 31 | type: 'success', 32 | summary: 'Credential has been successfully deleted.' 33 | } 34 | } 35 | } 36 | }; 37 | 38 | export const WithErrorMessage: Story = { 39 | globals: { 40 | kcContext: { 41 | message: { 42 | type: 'error', 43 | summary: 'Failed to delete the credential. Please try again.' 44 | } 45 | } 46 | } 47 | }; 48 | 49 | export const WithDisabledDeleteButton: Story = { 50 | globals: { 51 | kcContext: { 52 | isDeleteButtonDisabled: true, 53 | credentialLabel: 'Non-deletable Credential' 54 | } 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/login/pages/login-idp-link-email/login-idp-link-email.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, forwardRef, inject, type TemplateRef, viewChild } from '@angular/core'; 2 | import { ComponentReference } from '@keycloakify/angular/login/classes/component-reference'; 3 | import type { I18n } from '@keycloakify/angular/login/i18n'; 4 | import type { KcContext } from '@keycloakify/angular/login/KcContext'; 5 | import { LOGIN_I18N } from '@keycloakify/angular/login/tokens/i18n'; 6 | import { KC_LOGIN_CONTEXT } from '@keycloakify/angular/login/tokens/kc-context'; 7 | 8 | @Component({ 9 | selector: 'kc-login-idp-link-email', 10 | templateUrl: 'login-idp-link-email.component.html', 11 | changeDetection: ChangeDetectionStrategy.OnPush, 12 | providers: [ 13 | { 14 | provide: ComponentReference, 15 | useExisting: forwardRef(() => LoginIdpLinkEmailComponent) 16 | } 17 | ] 18 | }) 19 | export class LoginIdpLinkEmailComponent extends ComponentReference { 20 | kcContext = inject>(KC_LOGIN_CONTEXT); 21 | i18n = inject(LOGIN_I18N); 22 | 23 | documentTitle: string | undefined; 24 | bodyClassName: string | undefined; 25 | 26 | displayRequiredFields = false; 27 | displayInfo = false; 28 | displayMessage = false; 29 | 30 | headerNode = viewChild>('headerNode'); 31 | infoNode = viewChild>('infoNode'); 32 | socialProvidersNode = viewChild>('socialProvidersNode'); 33 | } 34 | -------------------------------------------------------------------------------- /src/login/components/field-errors/field-errors.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, forwardRef, inject, input } from '@angular/core'; 2 | import { USE_DEFAULT_CSS } from '@keycloakify/angular/lib/tokens/use-default-css'; 3 | import { ComponentReference } from '@keycloakify/angular/login/classes/component-reference'; 4 | import { KcClassDirective } from '@keycloakify/angular/login/directives/kc-class'; 5 | import type { FormFieldError } from '@keycloakify/angular/login/services/user-profile-form'; 6 | import { LOGIN_CLASSES } from '@keycloakify/angular/login/tokens/classes'; 7 | import type { Attribute } from 'keycloakify/login/KcContext'; 8 | import type { ClassKey } from 'keycloakify/login/lib/kcClsx'; 9 | 10 | @Component({ 11 | styles: [ 12 | ` 13 | :host { 14 | display: contents; 15 | } 16 | ` 17 | ], 18 | imports: [KcClassDirective], 19 | selector: 'kc-field-errors', 20 | templateUrl: 'field-errors.component.html', 21 | changeDetection: ChangeDetectionStrategy.OnPush, 22 | providers: [ 23 | { 24 | provide: ComponentReference, 25 | useExisting: forwardRef(() => FieldErrorsComponent) 26 | } 27 | ] 28 | }) 29 | export class FieldErrorsComponent extends ComponentReference { 30 | attribute = input(); 31 | displayableErrors = input(); 32 | fieldIndex = input(); 33 | override doUseDefaultCss = inject(USE_DEFAULT_CSS); 34 | override classes = inject>>(LOGIN_CLASSES); 35 | } 36 | -------------------------------------------------------------------------------- /src/account/pages/sessions/sessions.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, forwardRef, inject } from '@angular/core'; 2 | import { ComponentReference } from '@keycloakify/angular/account/classes/component-reference'; 3 | import { KcClassDirective } from '@keycloakify/angular/account/directives/kc-class'; 4 | import type { I18n } from '@keycloakify/angular/account/i18n'; 5 | import type { KcContext } from '@keycloakify/angular/account/KcContext'; 6 | import { ACCOUNT_CLASSES } from '@keycloakify/angular/account/tokens/classes'; 7 | import { ACCOUNT_I18N } from '@keycloakify/angular/account/tokens/i18n'; 8 | import { KC_ACCOUNT_CONTEXT } from '@keycloakify/angular/account/tokens/kc-context'; 9 | import { USE_DEFAULT_CSS } from '@keycloakify/angular/lib/tokens/use-default-css'; 10 | import type { ClassKey } from 'keycloakify/account'; 11 | 12 | @Component({ 13 | imports: [KcClassDirective], 14 | selector: 'kc-sessions', 15 | templateUrl: 'sessions.component.html', 16 | changeDetection: ChangeDetectionStrategy.OnPush, 17 | providers: [ 18 | { 19 | provide: ComponentReference, 20 | useExisting: forwardRef(() => SessionsComponent) 21 | } 22 | ] 23 | }) 24 | export class SessionsComponent extends ComponentReference { 25 | i18n = inject(ACCOUNT_I18N); 26 | kcContext = inject>(KC_ACCOUNT_CONTEXT); 27 | override doUseDefaultCss = inject(USE_DEFAULT_CSS); 28 | override classes = inject>>(ACCOUNT_CLASSES); 29 | active = 'sessions'; 30 | } 31 | -------------------------------------------------------------------------------- /src/account/pages/log/log.component.ts: -------------------------------------------------------------------------------- 1 | import { DatePipe } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, forwardRef, inject } from '@angular/core'; 3 | import { ComponentReference } from '@keycloakify/angular/account/classes/component-reference'; 4 | import { KcClassDirective } from '@keycloakify/angular/account/directives/kc-class'; 5 | import type { I18n } from '@keycloakify/angular/account/i18n'; 6 | import type { KcContext } from '@keycloakify/angular/account/KcContext'; 7 | import { ACCOUNT_CLASSES } from '@keycloakify/angular/account/tokens/classes'; 8 | import { ACCOUNT_I18N } from '@keycloakify/angular/account/tokens/i18n'; 9 | import { KC_ACCOUNT_CONTEXT } from '@keycloakify/angular/account/tokens/kc-context'; 10 | import { USE_DEFAULT_CSS } from '@keycloakify/angular/lib/tokens/use-default-css'; 11 | import type { ClassKey } from 'keycloakify/account'; 12 | 13 | @Component({ 14 | imports: [KcClassDirective, DatePipe], 15 | selector: 'kc-log', 16 | templateUrl: 'log.component.html', 17 | changeDetection: ChangeDetectionStrategy.OnPush, 18 | providers: [ 19 | { 20 | provide: ComponentReference, 21 | useExisting: forwardRef(() => LogComponent) 22 | } 23 | ] 24 | }) 25 | export class LogComponent extends ComponentReference { 26 | i18n = inject(ACCOUNT_I18N); 27 | kcContext = inject>(KC_ACCOUNT_CONTEXT); 28 | override doUseDefaultCss = inject(USE_DEFAULT_CSS); 29 | override classes = inject>>(ACCOUNT_CLASSES); 30 | active = 'log'; 31 | } 32 | -------------------------------------------------------------------------------- /stories/login/pages/login-config-totp.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/login-config-totp.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { 9 | pageId: 'login-config-totp.ftl' 10 | } 11 | }; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const Default: Story = {}; 17 | 18 | export const WithManualSetUp: Story = { 19 | globals: { 20 | kcContext: { 21 | mode: 'manual' 22 | } 23 | } 24 | }; 25 | 26 | export const WithError: Story = { 27 | globals: { 28 | kcContext: { 29 | messagesPerField: { 30 | get: (fieldName: string) => 31 | fieldName === 'totp' ? 'Invalid TOTP' : undefined, 32 | exists: (fieldName: string) => fieldName === 'totp', 33 | existsError: (fieldName: string) => fieldName === 'totp', 34 | printIfExists: (fieldName: string, x: T) => 35 | fieldName === 'totp' ? x : undefined 36 | } 37 | } 38 | } 39 | }; 40 | 41 | export const WithAppInitiatedAction: Story = { 42 | globals: { 43 | kcContext: { 44 | isAppInitiatedAction: true 45 | } 46 | } 47 | }; 48 | 49 | export const WithPreFilledUserLabel: Story = { 50 | globals: { 51 | kcContext: { 52 | totp: { 53 | otpCredentials: [{ userLabel: 'MyDevice' }] 54 | } 55 | } 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /src/account/pages/totp/totp.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, forwardRef, inject } from '@angular/core'; 2 | import { ComponentReference } from '@keycloakify/angular/account/classes/component-reference'; 3 | import { KcClassDirective } from '@keycloakify/angular/account/directives/kc-class'; 4 | import type { I18n } from '@keycloakify/angular/account/i18n'; 5 | import type { KcContext } from '@keycloakify/angular/account/KcContext'; 6 | import { ACCOUNT_CLASSES } from '@keycloakify/angular/account/tokens/classes'; 7 | import { ACCOUNT_I18N } from '@keycloakify/angular/account/tokens/i18n'; 8 | import { KC_ACCOUNT_CONTEXT } from '@keycloakify/angular/account/tokens/kc-context'; 9 | import { KcSanitizePipe } from '@keycloakify/angular/lib/pipes/kc-sanitize'; 10 | import { USE_DEFAULT_CSS } from '@keycloakify/angular/lib/tokens/use-default-css'; 11 | import type { ClassKey } from 'keycloakify/account'; 12 | 13 | @Component({ 14 | imports: [KcClassDirective, KcSanitizePipe], 15 | selector: 'kc-totp', 16 | templateUrl: 'totp.component.html', 17 | changeDetection: ChangeDetectionStrategy.OnPush, 18 | providers: [ 19 | { 20 | provide: ComponentReference, 21 | useExisting: forwardRef(() => TotpComponent) 22 | } 23 | ] 24 | }) 25 | export class TotpComponent extends ComponentReference { 26 | i18n = inject(ACCOUNT_I18N); 27 | kcContext = inject>(KC_ACCOUNT_CONTEXT); 28 | override doUseDefaultCss = inject(USE_DEFAULT_CSS); 29 | override classes = inject>>(ACCOUNT_CLASSES); 30 | active = 'totp'; 31 | } 32 | -------------------------------------------------------------------------------- /scripts/tools/StatefulObservable.ts: -------------------------------------------------------------------------------- 1 | export type StatefulObservable = { 2 | current: T; 3 | subscribe: (next: (data: T) => void) => Subscription; 4 | }; 5 | 6 | export type StatefulReadonlyObservable = { 7 | readonly current: T; 8 | subscribe: (next: (data: T) => void) => Subscription; 9 | }; 10 | 11 | export type Subscription = { 12 | unsubscribe(): void; 13 | }; 14 | 15 | export function createStatefulObservable( 16 | getInitialValue: () => T 17 | ): StatefulObservable { 18 | let nextFunctions: ((data: T) => void)[] = []; 19 | 20 | const { get, set } = (() => { 21 | let wrappedState: [T] | undefined = undefined; 22 | 23 | return { 24 | get: () => { 25 | if (wrappedState === undefined) { 26 | wrappedState = [getInitialValue()]; 27 | } 28 | return wrappedState[0]; 29 | }, 30 | set: (data: T) => { 31 | wrappedState = [data]; 32 | 33 | nextFunctions.forEach(next => next(data)); 34 | } 35 | }; 36 | })(); 37 | 38 | return Object.defineProperty( 39 | { 40 | current: null as any as T, 41 | subscribe: (next: (data: T) => void) => { 42 | nextFunctions.push(next); 43 | 44 | return { 45 | unsubscribe: () => 46 | nextFunctions.splice(nextFunctions.indexOf(next), 1) 47 | }; 48 | } 49 | }, 50 | 'current', 51 | { 52 | enumerable: true, 53 | get, 54 | set 55 | } 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /src/login/pages/login-oauth-grant/login-oauth-grant.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, forwardRef, inject, type TemplateRef, viewChild } from '@angular/core'; 2 | import { ComponentReference } from '@keycloakify/angular/login/classes/component-reference'; 3 | import { KcClassDirective } from '@keycloakify/angular/login/directives/kc-class'; 4 | import type { I18n } from '@keycloakify/angular/login/i18n'; 5 | import type { KcContext } from '@keycloakify/angular/login/KcContext'; 6 | import { LOGIN_I18N } from '@keycloakify/angular/login/tokens/i18n'; 7 | import { KC_LOGIN_CONTEXT } from '@keycloakify/angular/login/tokens/kc-context'; 8 | 9 | @Component({ 10 | imports: [KcClassDirective], 11 | selector: 'kc-login-oauth-grant', 12 | templateUrl: 'login-oauth-grant.component.html', 13 | changeDetection: ChangeDetectionStrategy.OnPush, 14 | providers: [ 15 | { 16 | provide: ComponentReference, 17 | useExisting: forwardRef(() => LoginOauthGrantComponent) 18 | } 19 | ] 20 | }) 21 | export class LoginOauthGrantComponent extends ComponentReference { 22 | kcContext = inject>(KC_LOGIN_CONTEXT); 23 | i18n = inject(LOGIN_I18N); 24 | 25 | documentTitle: string | undefined; 26 | bodyClassName = 'oauth'; 27 | 28 | displayRequiredFields = false; 29 | displayInfo = false; 30 | displayMessage = false; 31 | 32 | headerNode = viewChild>('headerNode'); 33 | infoNode = viewChild>('infoNode'); 34 | socialProvidersNode = viewChild>('socialProvidersNode'); 35 | } 36 | -------------------------------------------------------------------------------- /src/login/pages/delete-account-confirm/delete-account-confirm.component.html: -------------------------------------------------------------------------------- 1 | @let url = kcContext.url; 2 | @let triggered_from_aia = kcContext.triggered_from_aia; 3 | 4 | {{ i18n.msgStr('deleteAccountConfirm') }} 5 | 6 |
11 |
15 | 16 | {{ i18n.msgStr('irreversibleAction') }} 17 |
18 |

{{ i18n.msgStr('deletingImplies') }}

19 |
    20 |
  • {{ i18n.msgStr('loggingOutImmediately') }}
  • 21 |
  • {{ i18n.msgStr('errasingData') }}
  • 22 |
23 | 26 |
27 | 32 | @if (triggered_from_aia) { 33 | 42 | } 43 |
44 |
45 | -------------------------------------------------------------------------------- /src/account/defaultPage.ts: -------------------------------------------------------------------------------- 1 | import type { Type } from '@angular/core'; 2 | import type { KcContext } from '@keycloakify/angular/account/KcContext'; 3 | 4 | export function getDefaultPageComponent( 5 | pageId: KcContext['pageId'] 6 | ): Promise> { 7 | switch (pageId) { 8 | case 'password.ftl': 9 | return import('@keycloakify/angular/account/pages/password').then( 10 | c => c.PasswordComponent 11 | ); 12 | break; 13 | case 'account.ftl': 14 | return import('@keycloakify/angular/account/pages/account').then( 15 | c => c.AccountComponent 16 | ); 17 | break; 18 | case 'sessions.ftl': 19 | return import('@keycloakify/angular/account/pages/sessions').then( 20 | c => c.SessionsComponent 21 | ); 22 | break; 23 | case 'totp.ftl': 24 | return import('@keycloakify/angular/account/pages/totp').then( 25 | c => c.TotpComponent 26 | ); 27 | break; 28 | case 'applications.ftl': 29 | return import('@keycloakify/angular/account/pages/applications').then( 30 | c => c.ApplicationsComponent 31 | ); 32 | break; 33 | case 'log.ftl': 34 | return import('@keycloakify/angular/account/pages/log').then( 35 | c => c.LogComponent 36 | ); 37 | break; 38 | case 'federatedIdentity.ftl': 39 | return import('@keycloakify/angular/account/pages/federatedIdentity').then( 40 | c => c.FederatedIdentityComponent 41 | ); 42 | break; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "keycloakify-angular", 3 | "projectOwner": "keycloakify", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": ["README.md"], 7 | "imageSize": 100, 8 | "commit": false, 9 | "commitConvention": "angular", 10 | "contributors": [ 11 | { 12 | "login": "kathari00", 13 | "name": "Katharina E.", 14 | "avatar_url": "https://avatars.githubusercontent.com/u/42547712?v=4", 15 | "profile": "https://github.com/kathari00", 16 | "contributions": ["code", "test", "doc"] 17 | }, 18 | { 19 | "login": "luca-peruzzo", 20 | "name": "Luca Peruzzo", 21 | "avatar_url": "https://avatars.githubusercontent.com/u/69015314?v=4", 22 | "profile": "https://github.com/luca-peruzzo", 23 | "contributions": ["code", "test"] 24 | }, 25 | { 26 | "login": "garronej", 27 | "name": "Joseph Garrone", 28 | "avatar_url": "https://avatars.githubusercontent.com/u/6702424?v=4", 29 | "profile": "https://github.com/garronej", 30 | "contributions": ["code"] 31 | }, 32 | { 33 | "login": "lekhmanrus", 34 | "name": "Ruslan Lekhman", 35 | "avatar_url": "https://avatars.githubusercontent.com/u/2744696?v=4", 36 | "profile": "https://github.com/lekhmanrus", 37 | "contributions": ["code"] 38 | }, 39 | { 40 | "login": "vikashkumar29", 41 | "name": "Vikash Kumar", 42 | "avatar_url": "https://avatars.githubusercontent.com/u/513033?v=4", 43 | "profile": "https://github.com/vikashkumar29", 44 | "contributions": ["code"] 45 | } 46 | ], 47 | "contributorsPerLine": 7, 48 | "linkToUsage": false, 49 | "skipCi": true, 50 | "commitType": "docs" 51 | } 52 | 53 | -------------------------------------------------------------------------------- /src/login/pages/logout-confirm/logout-confirm.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, forwardRef, inject, type TemplateRef, viewChild } from '@angular/core'; 2 | import { KcSanitizePipe } from '@keycloakify/angular/lib/pipes/kc-sanitize'; 3 | import { ComponentReference } from '@keycloakify/angular/login/classes/component-reference'; 4 | import { KcClassDirective } from '@keycloakify/angular/login/directives/kc-class'; 5 | import type { I18n } from '@keycloakify/angular/login/i18n'; 6 | import type { KcContext } from '@keycloakify/angular/login/KcContext'; 7 | import { LOGIN_I18N } from '@keycloakify/angular/login/tokens/i18n'; 8 | import { KC_LOGIN_CONTEXT } from '@keycloakify/angular/login/tokens/kc-context'; 9 | 10 | @Component({ 11 | imports: [KcSanitizePipe, KcClassDirective], 12 | selector: 'kc-logout-confirm', 13 | templateUrl: 'logout-confirm.component.html', 14 | changeDetection: ChangeDetectionStrategy.OnPush, 15 | providers: [ 16 | { 17 | provide: ComponentReference, 18 | useExisting: forwardRef(() => LogoutConfirmComponent) 19 | } 20 | ] 21 | }) 22 | export class LogoutConfirmComponent extends ComponentReference { 23 | kcContext = inject>(KC_LOGIN_CONTEXT); 24 | i18n = inject(LOGIN_I18N); 25 | 26 | documentTitle: string | undefined; 27 | bodyClassName: string | undefined; 28 | 29 | displayRequiredFields = false; 30 | displayInfo = false; 31 | displayMessage = true; 32 | 33 | headerNode = viewChild>('headerNode'); 34 | infoNode = viewChild>('infoNode'); 35 | socialProvidersNode = viewChild>('socialProvidersNode'); 36 | } 37 | -------------------------------------------------------------------------------- /src/login/pages/update-email/update-email.component.html: -------------------------------------------------------------------------------- 1 | @let url = kcContext.url; 2 | @let isAppInitiatedAction = kcContext.isAppInitiatedAction; 3 | 4 | 5 | {{ i18n.msgStr('updateEmailTitle') }} 6 | 7 |
13 | 14 |
15 |
19 |
20 |
21 | 22 |
26 | 37 | @if (isAppInitiatedAction) { 38 | 46 | } 47 |
48 |
49 | 50 | -------------------------------------------------------------------------------- /src/login/pages/login-idp-link-confirm/login-idp-link-confirm.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, forwardRef, inject, type TemplateRef, viewChild } from '@angular/core'; 2 | import { ComponentReference } from '@keycloakify/angular/login/classes/component-reference'; 3 | import { KcClassDirective } from '@keycloakify/angular/login/directives/kc-class'; 4 | import type { I18n } from '@keycloakify/angular/login/i18n'; 5 | import type { KcContext } from '@keycloakify/angular/login/KcContext'; 6 | import { LOGIN_I18N } from '@keycloakify/angular/login/tokens/i18n'; 7 | import { KC_LOGIN_CONTEXT } from '@keycloakify/angular/login/tokens/kc-context'; 8 | 9 | @Component({ 10 | imports: [KcClassDirective], 11 | selector: 'kc-login-idp-link-confirm', 12 | templateUrl: 'login-idp-link-confirm.component.html', 13 | changeDetection: ChangeDetectionStrategy.OnPush, 14 | providers: [ 15 | { 16 | provide: ComponentReference, 17 | useExisting: forwardRef(() => LoginIdpLinkConfirmComponent) 18 | } 19 | ] 20 | }) 21 | export class LoginIdpLinkConfirmComponent extends ComponentReference { 22 | kcContext = inject>(KC_LOGIN_CONTEXT); 23 | i18n = inject(LOGIN_I18N); 24 | 25 | documentTitle: string | undefined; 26 | bodyClassName: string | undefined; 27 | 28 | displayRequiredFields = false; 29 | displayInfo = false; 30 | displayMessage = !this.kcContext.messagesPerField.existsError('totp', 'userLabel'); 31 | 32 | headerNode = viewChild>('headerNode'); 33 | infoNode = viewChild>('infoNode'); 34 | socialProvidersNode = viewChild>('socialProvidersNode'); 35 | } 36 | -------------------------------------------------------------------------------- /src/login/pages/login-update-profile/login-update-profile.component.html: -------------------------------------------------------------------------------- 1 | @let url = kcContext.url; 2 | @let isAppInitiatedAction = kcContext.isAppInitiatedAction; 3 | 4 | 5 | {{ i18n.msgStr('loginProfileTitle') }} 6 | 7 |
13 | 14 |
15 |
19 |
20 |
21 |
25 | 36 | @if (isAppInitiatedAction) { 37 | 46 | } 47 |
48 |
49 | 50 | -------------------------------------------------------------------------------- /stories/login/pages/webauthn-error.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/webauthn-error.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { 9 | pageId: 'webauthn-error.ftl' 10 | } 11 | }; 12 | 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | export const Default: Story = {}; 18 | 19 | export const WithRetryAvailable: Story = { 20 | globals: { 21 | kcContext: { 22 | url: { 23 | loginAction: '/mock-login-action' 24 | }, 25 | isAppInitiatedAction: false, 26 | message: { 27 | summary: 'WebAuthn authentication failed. Please try again.', 28 | type: 'error' 29 | } 30 | } 31 | } 32 | }; 33 | 34 | export const WithAppInitiatedAction: Story = { 35 | globals: { 36 | kcContext: { 37 | url: { 38 | loginAction: '/mock-login-action' 39 | }, 40 | isAppInitiatedAction: true, 41 | message: { 42 | summary: 'WebAuthn authentication failed. You can try again or cancel.', 43 | type: 'error' 44 | } 45 | } 46 | } 47 | }; 48 | 49 | export const WithJavaScriptDisabled: Story = { 50 | globals: { 51 | kcContext: { 52 | url: { 53 | loginAction: '/mock-login-action' 54 | }, 55 | isAppInitiatedAction: false, 56 | message: { 57 | summary: 'JavaScript is disabled or not working. Please retry manually.', 58 | type: 'warning' 59 | } 60 | } 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /src/account/pages/account/account.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, forwardRef, inject } from '@angular/core'; 2 | import { ComponentReference } from '@keycloakify/angular/account/classes/component-reference'; 3 | import { KcClassDirective } from '@keycloakify/angular/account/directives/kc-class'; 4 | import type { I18n } from '@keycloakify/angular/account/i18n'; 5 | import type { KcContext } from '@keycloakify/angular/account/KcContext'; 6 | import { ACCOUNT_CLASSES } from '@keycloakify/angular/account/tokens/classes'; 7 | import { ACCOUNT_I18N } from '@keycloakify/angular/account/tokens/i18n'; 8 | import { KC_ACCOUNT_CONTEXT } from '@keycloakify/angular/account/tokens/kc-context'; 9 | import { KcSanitizePipe } from '@keycloakify/angular/lib/pipes/kc-sanitize'; 10 | import { USE_DEFAULT_CSS } from '@keycloakify/angular/lib/tokens/use-default-css'; 11 | import type { ClassKey } from 'keycloakify/account'; 12 | 13 | @Component({ 14 | imports: [KcClassDirective, KcSanitizePipe], 15 | selector: 'kc-account', 16 | templateUrl: 'account.component.html', 17 | changeDetection: ChangeDetectionStrategy.OnPush, 18 | providers: [ 19 | { 20 | provide: ComponentReference, 21 | useExisting: forwardRef(() => AccountComponent) 22 | } 23 | ] 24 | }) 25 | export class AccountComponent extends ComponentReference { 26 | i18n = inject(ACCOUNT_I18N); 27 | kcContext = inject>(KC_ACCOUNT_CONTEXT); 28 | override doUseDefaultCss = inject(USE_DEFAULT_CSS); 29 | override classes = inject>>(ACCOUNT_CLASSES); 30 | override additionalClasses: Partial> = { 31 | kcBodyClass: `${this.classes?.kcBodyClass} user` 32 | }; 33 | active = 'account'; 34 | } 35 | -------------------------------------------------------------------------------- /src/account/pages/applications/applications.component.ts: -------------------------------------------------------------------------------- 1 | import { KeyValuePipe } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, forwardRef, inject } from '@angular/core'; 3 | import { ComponentReference } from '@keycloakify/angular/account/classes/component-reference'; 4 | import { KcClassDirective } from '@keycloakify/angular/account/directives/kc-class'; 5 | import type { I18n } from '@keycloakify/angular/account/i18n'; 6 | import type { KcContext } from '@keycloakify/angular/account/KcContext'; 7 | import { ACCOUNT_CLASSES } from '@keycloakify/angular/account/tokens/classes'; 8 | import { ACCOUNT_I18N } from '@keycloakify/angular/account/tokens/i18n'; 9 | import { KC_ACCOUNT_CONTEXT } from '@keycloakify/angular/account/tokens/kc-context'; 10 | import { IsArrayWithEmptyObjectPipe } from '@keycloakify/angular/lib/pipes/is-array-with-empty-object'; 11 | import { USE_DEFAULT_CSS } from '@keycloakify/angular/lib/tokens/use-default-css'; 12 | import type { ClassKey } from 'keycloakify/account'; 13 | 14 | @Component({ 15 | imports: [KcClassDirective, KeyValuePipe, IsArrayWithEmptyObjectPipe], 16 | selector: 'kc-applications', 17 | templateUrl: 'applications.component.html', 18 | changeDetection: ChangeDetectionStrategy.OnPush, 19 | providers: [ 20 | { 21 | provide: ComponentReference, 22 | useExisting: forwardRef(() => ApplicationsComponent) 23 | } 24 | ] 25 | }) 26 | export class ApplicationsComponent extends ComponentReference { 27 | i18n = inject(ACCOUNT_I18N); 28 | kcContext = inject>(KC_ACCOUNT_CONTEXT); 29 | override doUseDefaultCss = inject(USE_DEFAULT_CSS); 30 | override classes = inject>>(ACCOUNT_CLASSES); 31 | active = 'applications'; 32 | } 33 | -------------------------------------------------------------------------------- /src/login/pages/login-idp-link-confirm-override/login-idp-link-confirm-override.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, forwardRef, inject, type TemplateRef, viewChild } from '@angular/core'; 2 | import { ComponentReference } from '@keycloakify/angular/login/classes/component-reference'; 3 | import { KcClassDirective } from '@keycloakify/angular/login/directives/kc-class'; 4 | import type { I18n } from '@keycloakify/angular/login/i18n'; 5 | import type { KcContext } from '@keycloakify/angular/login/KcContext'; 6 | import { LOGIN_I18N } from '@keycloakify/angular/login/tokens/i18n'; 7 | import { KC_LOGIN_CONTEXT } from '@keycloakify/angular/login/tokens/kc-context'; 8 | 9 | @Component({ 10 | imports: [KcClassDirective], 11 | selector: 'kc-login-idp-link-confirm-override', 12 | templateUrl: 'login-idp-link-confirm-override.component.html', 13 | changeDetection: ChangeDetectionStrategy.OnPush, 14 | providers: [ 15 | { 16 | provide: ComponentReference, 17 | useExisting: forwardRef(() => LoginIdpLinkConfirmOverrideComponent) 18 | } 19 | ] 20 | }) 21 | export class LoginIdpLinkConfirmOverrideComponent extends ComponentReference { 22 | kcContext = inject>(KC_LOGIN_CONTEXT); 23 | i18n = inject(LOGIN_I18N); 24 | 25 | documentTitle: string | undefined; 26 | bodyClassName: string | undefined; 27 | 28 | displayRequiredFields = false; 29 | displayInfo = false; 30 | displayMessage = !this.kcContext.messagesPerField.existsError('totp', 'userLabel'); 31 | 32 | headerNode = viewChild>('headerNode'); 33 | infoNode = viewChild>('infoNode'); 34 | socialProvidersNode = viewChild>('socialProvidersNode'); 35 | } 36 | -------------------------------------------------------------------------------- /src/login/pages/logout-confirm/logout-confirm.component.html: -------------------------------------------------------------------------------- 1 | @let url = kcContext.url; 2 | @let logoutConfirm = kcContext.logoutConfirm; 3 | @let client = kcContext.client; 4 | 5 | 6 | {{ i18n.msgStr('logoutConfirmTitle') }} 7 | 8 |
12 |

{{ i18n.msgStr('logoutConfirmHeader') }}

13 |
18 | 23 |
24 |
25 |
26 |
27 |
31 | 39 |
40 |
41 |
42 |
43 | @if (!logoutConfirm.skipLink && client.baseUrl) { 44 |

45 | 49 | 50 |

51 | } 52 |
53 |
54 | -------------------------------------------------------------------------------- /src/login/pages/select-authenticator/select-authenticator.component.html: -------------------------------------------------------------------------------- 1 | @let url = kcContext.url; 2 | @let auth = kcContext.auth; 3 | 4 | 5 | {{ i18n.msgStr('loginChooseAuthenticator') }} 6 | 7 | 8 |
14 |
15 | @for (authenticationSelection of auth.authenticationSelections; track authenticationSelection; let i = $index) { 16 | 38 | } 39 |
40 |
41 | -------------------------------------------------------------------------------- /src/account/services/account-resource-injector/account-resource-injector.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core'; 2 | import { KC_ACCOUNT_CONTEXT } from '@keycloakify/angular/account/tokens/kc-context'; 3 | import { type Script } from '@keycloakify/angular/lib/models/script'; 4 | import { ResourceInjectorService } from '@keycloakify/angular/lib/services/resource-injector'; 5 | import { catchError, forkJoin, of, switchMap } from 'rxjs'; 6 | import type { KcContext } from '@keycloakify/angular/account/KcContext'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class AccountResourceInjectorService { 12 | private kcContext: KcContext = inject(KC_ACCOUNT_CONTEXT); 13 | private resourceInjectorService: ResourceInjectorService = inject( 14 | ResourceInjectorService 15 | ); 16 | 17 | injectResource(doUseDefaultCss = true) { 18 | if (!doUseDefaultCss) { 19 | return of(true); 20 | } 21 | const stylesheets = [ 22 | `${this.kcContext.url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`, 23 | `${this.kcContext.url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`, 24 | `${this.kcContext.url.resourcesPath}/css/account.css` 25 | ]; 26 | 27 | return forkJoin( 28 | stylesheets.map(url => this.resourceInjectorService.createLink(url)) 29 | ).pipe( 30 | switchMap(() => of(true)), 31 | catchError(error => { 32 | console.error('Error loading styles:', error); 33 | return of(false); 34 | }) 35 | ); 36 | } 37 | 38 | insertAdditionalScripts(scripts: Script[]) { 39 | scripts.map(script => this.resourceInjectorService.createScript(script)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/login/pages/login-oauth2-device-verify-user-code/login-oauth2-device-verify-user-code.component.html: -------------------------------------------------------------------------------- 1 | @let url = kcContext.url; 2 | 3 | 4 | {{ i18n.msgStr('oauth2DeviceVerificationTitle') }} 5 | 6 |
12 |
13 |
14 | 20 |
21 | 22 |
23 | 31 |
32 |
33 |
34 |
38 |
39 |
40 | 41 |
45 |
46 | 51 |
52 |
53 |
54 |
55 | -------------------------------------------------------------------------------- /stories/account/KcPageStory.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectorRef, 3 | Component, 4 | inject, 5 | type OnInit, 6 | type Type 7 | } from '@angular/core'; 8 | import type { KcContext } from '@keycloakify/angular/account/KcContext'; 9 | import { provideKeycloakifyAngular } from '@keycloakify/angular/account/providers/keycloakify-angular'; 10 | import { TemplateComponent } from '@keycloakify/angular/account/template'; 11 | import { KC_ACCOUNT_CONTEXT } from '@keycloakify/angular/account/tokens/kc-context'; 12 | import type { StoryContext } from '@storybook/angular'; 13 | import { getKcContextMock } from './KcContextMock'; 14 | import { getKcPage } from './KcPage'; 15 | import { getI18n } from './i18n'; 16 | 17 | export const decorators = (_: unknown, context: StoryContext) => ({ 18 | applicationConfig: { 19 | providers: [ 20 | provideKeycloakifyAngular({ 21 | doUseDefaultCss: true, 22 | classes: {}, 23 | kcContext: getKcContextMock({ 24 | pageId: context.globals['pageId'], 25 | overrides: context.globals['kcContext'] 26 | }), 27 | getI18n: getI18n 28 | }) 29 | ] 30 | } 31 | }); 32 | 33 | @Component({ 34 | selector: 'kc-page-story', 35 | template: `@if (pageComponent) { 36 | 37 | }`, 38 | imports: [TemplateComponent] 39 | }) 40 | export class KcPageStory implements OnInit { 41 | pageComponent: Type | undefined; 42 | kcContext = inject(KC_ACCOUNT_CONTEXT); 43 | readonly #cd = inject(ChangeDetectorRef); 44 | ngOnInit() { 45 | getKcPage(this.kcContext.pageId).then(kcPage => { 46 | this.pageComponent = kcPage.PageComponent; 47 | this.#cd.markForCheck(); 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /stories/login/pages/login-oauth-grant.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const mockKcContext = { 5 | url: { 6 | oauthAction: '/oauth-action' 7 | }, 8 | oauth: { 9 | clientScopesRequested: [ 10 | { consentScreenText: 'Scope1', dynamicScopeParameter: 'dynamicScope1' }, 11 | { consentScreenText: 'Scope2' } 12 | ], 13 | code: 'mockCode' 14 | }, 15 | client: { 16 | attributes: { 17 | policyUri: 'https://twitter.com/en/tos', 18 | tosUri: 'https://twitter.com/en/privacy' 19 | }, 20 | name: 'Twitter', 21 | clientId: 'twitter-client-id' 22 | } 23 | }; 24 | 25 | const meta: Meta = { 26 | title: 'login/login-oauth-grant.ftl', 27 | component: KcPageStory, 28 | decorators: decorators, 29 | globals: { 30 | pageId: 'login-oauth-grant.ftl' 31 | } 32 | }; 33 | 34 | export default meta; 35 | type Story = StoryObj; 36 | 37 | export const Default: Story = { 38 | globals: { 39 | kcContext: mockKcContext 40 | } 41 | }; 42 | 43 | export const WithoutScopes: Story = { 44 | globals: { 45 | kcContext: { 46 | ...mockKcContext, 47 | oauth: { 48 | ...mockKcContext.oauth, 49 | clientScopesRequested: [] 50 | } 51 | } 52 | } 53 | }; 54 | 55 | export const WithFormSubmissionError: Story = { 56 | globals: { 57 | kcContext: { 58 | ...mockKcContext, 59 | url: { 60 | oauthAction: '/error' 61 | }, 62 | message: { 63 | type: 'error', 64 | summary: 'An error occurred during form submission.' 65 | } 66 | } 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /src/account/pages/sessions/sessions.component.html: -------------------------------------------------------------------------------- 1 | @let url = kcContext.url; 2 | @let sessions = kcContext.sessions; 3 | @let stateChecker = kcContext.stateChecker; 4 | 5 |
6 |
7 |

{{ i18n.msgStr('sessionsHtmlTitle') }}

8 |
9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | @for (session of sessions.sessions; track session) { 24 | 25 | 26 | 27 | 28 | 29 | 37 | 38 | } 39 | 40 |
{{ i18n.msgStr('ip') }}{{ i18n.msgStr('started') }}{{ i18n.msgStr('lastAccess') }}{{ i18n.msgStr('expires') }}{{ i18n.msgStr('clients') }}
{{ session.ipAddress }}{{ session?.started }}{{ session?.lastAccess }}{{ session?.expires }} 30 | @for (client of session.clients; track client) { 31 |
32 | {{ client }} 33 |
34 |
35 | } 36 |
41 | 42 |
46 | 52 | 59 |
60 | -------------------------------------------------------------------------------- /stories/login/pages/idp-review-user-profile.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/angular'; 2 | import { decorators, KcPageStory } from '../KcPageStory'; 3 | 4 | const meta: Meta = { 5 | title: 'login/idp-review-user-profile.ftl', 6 | component: KcPageStory, 7 | decorators: decorators, 8 | globals: { 9 | pageId: 'idp-review-user-profile.ftl' 10 | } 11 | }; 12 | 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | export const Default: Story = {}; 18 | 19 | export const WithFormValidationErrors: Story = { 20 | globals: { 21 | kcContext: { 22 | messagesPerField: { 23 | existsError: (fieldName: string) => 24 | ['email', 'firstName'].includes(fieldName), 25 | get: (fieldName: string) => { 26 | if (fieldName === 'email') return 'Invalid email format.'; 27 | if (fieldName === 'firstName') return 'First name is required.'; 28 | return ''; 29 | } 30 | } 31 | } 32 | } 33 | }; 34 | 35 | export const WithReadOnlyFields: Story = { 36 | globals: { 37 | kcContext: { 38 | profile: { 39 | attributesByName: { 40 | email: { value: 'jane.doe@example.com', readOnly: true }, 41 | firstName: { value: 'Jane', readOnly: false } 42 | } 43 | } 44 | } 45 | } 46 | }; 47 | 48 | export const WithPrefilledFormFields: Story = { 49 | globals: { 50 | kcContext: { 51 | profile: { 52 | attributesByName: { 53 | firstName: { value: 'Jane' }, 54 | lastName: { value: 'Doe' }, 55 | email: { value: 'jane.doe@example.com' } 56 | } 57 | } 58 | } 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /src/login/pages/login-verify-email/login-verify-email.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, forwardRef, inject, type TemplateRef, viewChild } from '@angular/core'; 2 | import { USE_DEFAULT_CSS } from '@keycloakify/angular/lib/tokens/use-default-css'; 3 | import { ComponentReference } from '@keycloakify/angular/login/classes/component-reference'; 4 | import type { I18n } from '@keycloakify/angular/login/i18n'; 5 | import type { KcContext } from '@keycloakify/angular/login/KcContext'; 6 | import { LOGIN_CLASSES } from '@keycloakify/angular/login/tokens/classes'; 7 | import { LOGIN_I18N } from '@keycloakify/angular/login/tokens/i18n'; 8 | import { KC_LOGIN_CONTEXT } from '@keycloakify/angular/login/tokens/kc-context'; 9 | import type { ClassKey } from 'keycloakify/login/lib/kcClsx'; 10 | 11 | @Component({ 12 | selector: 'kc-login-verify-email', 13 | templateUrl: 'login-verify-email.component.html', 14 | changeDetection: ChangeDetectionStrategy.OnPush, 15 | providers: [ 16 | { 17 | provide: ComponentReference, 18 | useExisting: forwardRef(() => LoginVerifyEmailComponent) 19 | } 20 | ] 21 | }) 22 | export class LoginVerifyEmailComponent extends ComponentReference { 23 | kcContext = inject>(KC_LOGIN_CONTEXT); 24 | i18n = inject(LOGIN_I18N); 25 | override doUseDefaultCss = inject(USE_DEFAULT_CSS); 26 | override classes = inject>>(LOGIN_CLASSES); 27 | 28 | documentTitle: string | undefined; 29 | bodyClassName: string | undefined; 30 | 31 | displayRequiredFields = false; 32 | displayInfo = true; 33 | displayMessage = true; 34 | 35 | headerNode = viewChild>('headerNode'); 36 | infoNode = viewChild>('infoNode'); 37 | socialProvidersNode = viewChild>('socialProvidersNode'); 38 | } 39 | -------------------------------------------------------------------------------- /src/login/pages/login-page-expired/login-page-expired.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, forwardRef, inject, type TemplateRef, viewChild } from '@angular/core'; 2 | import { USE_DEFAULT_CSS } from '@keycloakify/angular/lib/tokens/use-default-css'; 3 | import { ComponentReference } from '@keycloakify/angular/login/classes/component-reference'; 4 | import type { I18n } from '@keycloakify/angular/login/i18n'; 5 | import type { KcContext } from '@keycloakify/angular/login/KcContext'; 6 | import { LOGIN_CLASSES } from '@keycloakify/angular/login/tokens/classes'; 7 | import { LOGIN_I18N } from '@keycloakify/angular/login/tokens/i18n'; 8 | import { KC_LOGIN_CONTEXT } from '@keycloakify/angular/login/tokens/kc-context'; 9 | import type { ClassKey } from 'keycloakify/login/lib/kcClsx'; 10 | 11 | @Component({ 12 | selector: 'kc-login-page-expired', 13 | templateUrl: 'login-page-expired.component.html', 14 | changeDetection: ChangeDetectionStrategy.OnPush, 15 | providers: [ 16 | { 17 | provide: ComponentReference, 18 | useExisting: forwardRef(() => LoginPageExpiredComponent) 19 | } 20 | ] 21 | }) 22 | export class LoginPageExpiredComponent extends ComponentReference { 23 | kcContext = inject>(KC_LOGIN_CONTEXT); 24 | i18n = inject(LOGIN_I18N); 25 | override doUseDefaultCss = inject(USE_DEFAULT_CSS); 26 | override classes = inject>>(LOGIN_CLASSES); 27 | 28 | documentTitle: string | undefined; 29 | bodyClassName: string | undefined; 30 | 31 | displayRequiredFields = false; 32 | displayInfo = false; 33 | displayMessage = false; 34 | 35 | headerNode = viewChild>('headerNode'); 36 | infoNode = viewChild>('infoNode'); 37 | socialProvidersNode = viewChild>('socialProvidersNode'); 38 | } 39 | -------------------------------------------------------------------------------- /src/login/pages/code/code.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, forwardRef, inject, type TemplateRef, viewChild } from '@angular/core'; 2 | import { USE_DEFAULT_CSS } from '@keycloakify/angular/lib/tokens/use-default-css'; 3 | import { ComponentReference } from '@keycloakify/angular/login/classes/component-reference'; 4 | import { KcClassDirective } from '@keycloakify/angular/login/directives/kc-class'; 5 | import type { I18n } from '@keycloakify/angular/login/i18n'; 6 | import type { KcContext } from '@keycloakify/angular/login/KcContext'; 7 | import { LOGIN_CLASSES } from '@keycloakify/angular/login/tokens/classes'; 8 | import { LOGIN_I18N } from '@keycloakify/angular/login/tokens/i18n'; 9 | import { KC_LOGIN_CONTEXT } from '@keycloakify/angular/login/tokens/kc-context'; 10 | import type { ClassKey } from 'keycloakify/login/lib/kcClsx'; 11 | 12 | @Component({ 13 | imports: [KcClassDirective], 14 | selector: 'kc-code', 15 | templateUrl: 'code.component.html', 16 | changeDetection: ChangeDetectionStrategy.OnPush, 17 | providers: [ 18 | { 19 | provide: ComponentReference, 20 | useExisting: forwardRef(() => CodeComponent) 21 | } 22 | ] 23 | }) 24 | export class CodeComponent extends ComponentReference { 25 | kcContext = inject>(KC_LOGIN_CONTEXT); 26 | i18n = inject(LOGIN_I18N); 27 | 28 | override doUseDefaultCss = inject(USE_DEFAULT_CSS); 29 | override classes = inject>>(LOGIN_CLASSES); 30 | 31 | documentTitle: string | undefined; 32 | bodyClassName: string | undefined; 33 | 34 | displayRequiredFields = false; 35 | displayInfo = false; 36 | displayMessage = false; 37 | 38 | headerNode = viewChild>('headerNode'); 39 | infoNode = viewChild>('infoNode'); 40 | socialProvidersNode = viewChild>('socialProvidersNode'); 41 | } 42 | -------------------------------------------------------------------------------- /src/login/pages/error/error.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, forwardRef, inject, type TemplateRef, viewChild } from '@angular/core'; 2 | import { KcSanitizePipe } from '@keycloakify/angular/lib/pipes/kc-sanitize'; 3 | import { USE_DEFAULT_CSS } from '@keycloakify/angular/lib/tokens/use-default-css'; 4 | import { ComponentReference } from '@keycloakify/angular/login/classes/component-reference'; 5 | import type { I18n } from '@keycloakify/angular/login/i18n'; 6 | import type { KcContext } from '@keycloakify/angular/login/KcContext'; 7 | import { LOGIN_CLASSES } from '@keycloakify/angular/login/tokens/classes'; 8 | import { LOGIN_I18N } from '@keycloakify/angular/login/tokens/i18n'; 9 | import { KC_LOGIN_CONTEXT } from '@keycloakify/angular/login/tokens/kc-context'; 10 | import type { ClassKey } from 'keycloakify/login/lib/kcClsx'; 11 | 12 | @Component({ 13 | imports: [KcSanitizePipe], 14 | selector: 'kc-error', 15 | templateUrl: 'error.component.html', 16 | changeDetection: ChangeDetectionStrategy.OnPush, 17 | providers: [ 18 | { 19 | provide: ComponentReference, 20 | useExisting: forwardRef(() => ErrorComponent) 21 | } 22 | ] 23 | }) 24 | export class ErrorComponent extends ComponentReference { 25 | kcContext = inject>(KC_LOGIN_CONTEXT); 26 | i18n = inject(LOGIN_I18N); 27 | 28 | override doUseDefaultCss = inject(USE_DEFAULT_CSS); 29 | override classes = inject>>(LOGIN_CLASSES); 30 | 31 | documentTitle: string | undefined; 32 | bodyClassName: string | undefined; 33 | 34 | displayRequiredFields = false; 35 | displayInfo = false; 36 | displayMessage = false; 37 | 38 | headerNode = viewChild>('headerNode'); 39 | infoNode = viewChild>('infoNode'); 40 | socialProvidersNode = viewChild>('socialProvidersNode'); 41 | } 42 | -------------------------------------------------------------------------------- /src/login/pages/terms/terms.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, forwardRef, inject, type TemplateRef, viewChild } from '@angular/core'; 2 | import { USE_DEFAULT_CSS } from '@keycloakify/angular/lib/tokens/use-default-css'; 3 | import { ComponentReference } from '@keycloakify/angular/login/classes/component-reference'; 4 | import { KcClassDirective } from '@keycloakify/angular/login/directives/kc-class'; 5 | import type { I18n } from '@keycloakify/angular/login/i18n'; 6 | import type { KcContext } from '@keycloakify/angular/login/KcContext'; 7 | import { LOGIN_CLASSES } from '@keycloakify/angular/login/tokens/classes'; 8 | import { LOGIN_I18N } from '@keycloakify/angular/login/tokens/i18n'; 9 | import { KC_LOGIN_CONTEXT } from '@keycloakify/angular/login/tokens/kc-context'; 10 | import type { ClassKey } from 'keycloakify/login/lib/kcClsx'; 11 | 12 | @Component({ 13 | imports: [KcClassDirective], 14 | selector: 'kc-terms', 15 | templateUrl: 'terms.component.html', 16 | changeDetection: ChangeDetectionStrategy.OnPush, 17 | providers: [ 18 | { 19 | provide: ComponentReference, 20 | useExisting: forwardRef(() => TermsComponent) 21 | } 22 | ] 23 | }) 24 | export class TermsComponent extends ComponentReference { 25 | kcContext = inject>(KC_LOGIN_CONTEXT); 26 | i18n = inject(LOGIN_I18N); 27 | 28 | override doUseDefaultCss = inject(USE_DEFAULT_CSS); 29 | override classes = inject>>(LOGIN_CLASSES); 30 | 31 | documentTitle: string | undefined; 32 | bodyClassName: string | undefined; 33 | 34 | displayRequiredFields = false; 35 | displayInfo = false; 36 | displayMessage = false; 37 | 38 | headerNode = viewChild>('headerNode'); 39 | infoNode = viewChild>('infoNode'); 40 | socialProvidersNode = viewChild>('socialProvidersNode'); 41 | } 42 | -------------------------------------------------------------------------------- /src/login/components/group-label/group-label.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, computed, forwardRef, inject, input } from '@angular/core'; 2 | import { USE_DEFAULT_CSS } from '@keycloakify/angular/lib/tokens/use-default-css'; 3 | import { ComponentReference } from '@keycloakify/angular/login/classes/component-reference'; 4 | import { AttributesDirective } from '@keycloakify/angular/lib/directives/attributes'; 5 | import { KcClassDirective } from '@keycloakify/angular/login/directives/kc-class'; 6 | import { LOGIN_CLASSES } from '@keycloakify/angular/login/tokens/classes'; 7 | import { LOGIN_I18N } from '@keycloakify/angular/login/tokens/i18n'; 8 | import type { Attribute } from 'keycloakify/login/KcContext'; 9 | import type { ClassKey } from 'keycloakify/login/lib/kcClsx'; 10 | import type { I18n } from '@keycloakify/angular/login/i18n'; 11 | 12 | @Component({ 13 | styles: [ 14 | ` 15 | :host { 16 | display: contents; 17 | } 18 | ` 19 | ], 20 | imports: [KcClassDirective, AttributesDirective], 21 | selector: 'kc-group-label', 22 | templateUrl: 'group-label.component.html', 23 | changeDetection: ChangeDetectionStrategy.OnPush, 24 | providers: [ 25 | { 26 | provide: ComponentReference, 27 | useExisting: forwardRef(() => GroupLabelComponent) 28 | } 29 | ] 30 | }) 31 | export class GroupLabelComponent extends ComponentReference { 32 | i18n = inject(LOGIN_I18N); 33 | attribute = input(); 34 | groupName = input(); 35 | override doUseDefaultCss = inject(USE_DEFAULT_CSS); 36 | override classes = inject>>(LOGIN_CLASSES); 37 | groupNameRef = computed(() => { 38 | const attribute = this.attribute(); 39 | const groupName = this.groupName(); 40 | if (attribute?.group?.name !== groupName) { 41 | return attribute?.group?.name ?? ''; 42 | } 43 | return ''; 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /src/login/components/input-tag/input-tag.component.html: -------------------------------------------------------------------------------- 1 | @let attr = attribute(); 2 | @let index = fieldIndex(); 3 | @if (attr) { 4 | 31 | @if (index !== undefined) { 32 | @let values = valueOrValues() ?? [] | toArray; 33 | 38 | 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /stories/login/KcPageStory.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectorRef, 3 | Component, 4 | inject, 5 | type OnInit, 6 | type Type 7 | } from '@angular/core'; 8 | import { provideKeycloakifyAngular } from '@keycloakify/angular/login/providers/keycloakify-angular'; 9 | import { TemplateComponent } from '@keycloakify/angular/login/template'; 10 | import { KC_LOGIN_CONTEXT } from '@keycloakify/angular/login/tokens/kc-context'; 11 | import type { StoryContext } from '@storybook/angular'; 12 | import { getI18n } from './i18n'; 13 | import { getKcContextMock } from './KcContextMock'; 14 | import { getKcPage } from './KcPage'; 15 | 16 | export const decorators = (_: unknown, context: StoryContext) => ({ 17 | applicationConfig: { 18 | providers: [ 19 | provideKeycloakifyAngular({ 20 | doUseDefaultCss: true, 21 | classes: {}, 22 | kcContext: getKcContextMock({ 23 | pageId: context.globals['pageId'], 24 | overrides: context.globals['kcContext'] 25 | }), 26 | getI18n: getI18n 27 | }) 28 | ] 29 | } 30 | }); 31 | 32 | @Component({ 33 | selector: 'kc-page-story', 34 | template: `@if (pageComponent) { 35 | 39 | }`, 40 | imports: [TemplateComponent] 41 | }) 42 | export class KcPageStory implements OnInit { 43 | pageComponent: Type | undefined; 44 | kcContext = inject(KC_LOGIN_CONTEXT); 45 | readonly #cd = inject(ChangeDetectorRef); 46 | userProfileFormFieldsComponent: Type | undefined; 47 | ngOnInit() { 48 | getKcPage(this.kcContext.pageId).then(kcPage => { 49 | this.pageComponent = kcPage.PageComponent; 50 | this.userProfileFormFieldsComponent = kcPage.UserProfileFormFieldsComponent; 51 | this.#cd.markForCheck(); 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/services/resource-injector/resource-injector.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable, type Renderer2, RendererFactory2 } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { type Script } from '@keycloakify/angular/lib/models/script'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class ResourceInjectorService { 9 | private renderer: Renderer2 = inject(RendererFactory2).createRenderer(null, null); 10 | 11 | createLink(url: string): Observable { 12 | return new Observable(observer => { 13 | // check if the style is already injected 14 | if (Array.from(document.styleSheets).some(s => s.href?.includes(url))) { 15 | observer.next(); 16 | observer.complete(); 17 | console.debug(`stylesheet: ${url} already loaded`); 18 | return; 19 | } 20 | const link = document.createElement('link'); 21 | link.rel = 'stylesheet'; 22 | link.href = url; 23 | 24 | link.onload = () => { 25 | observer.next(); 26 | observer.complete(); 27 | }; 28 | 29 | link.onerror = () => { 30 | observer.error(new Error(`Failed to load stylesheet: ${url}`)); 31 | }; 32 | 33 | const head = document.head; 34 | head.insertBefore(link, head.firstChild); 35 | }); 36 | } 37 | createScript({ type, id, src, textContent }: Script): void { 38 | // check if the script is already injected 39 | if (Array.from(document.scripts).some(s => s.id?.includes(id))) { 40 | console.debug(`script: ${src} already injected`); 41 | return; 42 | } 43 | const script = document.createElement('script'); 44 | script.type = type; 45 | if (src) script.src = src; 46 | if (textContent) script.textContent = textContent; 47 | script.id = id; 48 | 49 | this.renderer.appendChild(document.head, script); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/login/pages/login-x509-info/login-x509-info.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, forwardRef, inject, type TemplateRef, viewChild } from '@angular/core'; 2 | import { USE_DEFAULT_CSS } from '@keycloakify/angular/lib/tokens/use-default-css'; 3 | import { ComponentReference } from '@keycloakify/angular/login/classes/component-reference'; 4 | import { KcClassDirective } from '@keycloakify/angular/login/directives/kc-class'; 5 | import type { I18n } from '@keycloakify/angular/login/i18n'; 6 | import type { KcContext } from '@keycloakify/angular/login/KcContext'; 7 | import { LOGIN_CLASSES } from '@keycloakify/angular/login/tokens/classes'; 8 | import { LOGIN_I18N } from '@keycloakify/angular/login/tokens/i18n'; 9 | import { KC_LOGIN_CONTEXT } from '@keycloakify/angular/login/tokens/kc-context'; 10 | import type { ClassKey } from 'keycloakify/login/lib/kcClsx'; 11 | 12 | @Component({ 13 | imports: [KcClassDirective], 14 | selector: 'kc-login-x509-info', 15 | templateUrl: 'login-x509-info.component.html', 16 | changeDetection: ChangeDetectionStrategy.OnPush, 17 | providers: [ 18 | { 19 | provide: ComponentReference, 20 | useExisting: forwardRef(() => LoginX509InfoComponent) 21 | } 22 | ] 23 | }) 24 | export class LoginX509InfoComponent extends ComponentReference { 25 | kcContext = inject>(KC_LOGIN_CONTEXT); 26 | i18n = inject(LOGIN_I18N); 27 | override doUseDefaultCss = inject(USE_DEFAULT_CSS); 28 | override classes = inject>>(LOGIN_CLASSES); 29 | 30 | documentTitle: string | undefined; 31 | bodyClassName: string | undefined; 32 | 33 | displayRequiredFields = false; 34 | displayInfo = false; 35 | displayMessage = false; 36 | 37 | headerNode = viewChild>('headerNode'); 38 | infoNode = viewChild>('infoNode'); 39 | socialProvidersNode = viewChild>('socialProvidersNode'); 40 | } 41 | -------------------------------------------------------------------------------- /src/login/pages/delete-credential/delete-credential.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, forwardRef, inject, type TemplateRef, viewChild } from '@angular/core'; 2 | import { USE_DEFAULT_CSS } from '@keycloakify/angular/lib/tokens/use-default-css'; 3 | import { ComponentReference } from '@keycloakify/angular/login/classes/component-reference'; 4 | import { KcClassDirective } from '@keycloakify/angular/login/directives/kc-class'; 5 | import type { I18n } from '@keycloakify/angular/login/i18n'; 6 | import type { KcContext } from '@keycloakify/angular/login/KcContext'; 7 | import { LOGIN_CLASSES } from '@keycloakify/angular/login/tokens/classes'; 8 | import { LOGIN_I18N } from '@keycloakify/angular/login/tokens/i18n'; 9 | import { KC_LOGIN_CONTEXT } from '@keycloakify/angular/login/tokens/kc-context'; 10 | import type { ClassKey } from 'keycloakify/login/lib/kcClsx'; 11 | 12 | @Component({ 13 | imports: [KcClassDirective], 14 | selector: 'kc-delete-credential', 15 | templateUrl: 'delete-credential.component.html', 16 | changeDetection: ChangeDetectionStrategy.OnPush, 17 | providers: [ 18 | { 19 | provide: ComponentReference, 20 | useExisting: forwardRef(() => DeleteCredentialComponent) 21 | } 22 | ] 23 | }) 24 | export class DeleteCredentialComponent extends ComponentReference { 25 | kcContext = inject>(KC_LOGIN_CONTEXT); 26 | i18n = inject(LOGIN_I18N); 27 | 28 | override doUseDefaultCss = inject(USE_DEFAULT_CSS); 29 | override classes = inject>>(LOGIN_CLASSES); 30 | 31 | documentTitle: string | undefined; 32 | bodyClassName: string | undefined; 33 | 34 | displayRequiredFields = false; 35 | displayInfo = false; 36 | displayMessage = false; 37 | 38 | headerNode = viewChild>('headerNode'); 39 | infoNode = viewChild>('infoNode'); 40 | socialProvidersNode = viewChild>('socialProvidersNode'); 41 | } 42 | -------------------------------------------------------------------------------- /src/login/pages/select-authenticator/select-authenticator.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, forwardRef, inject, type TemplateRef, viewChild } from '@angular/core'; 2 | import { USE_DEFAULT_CSS } from '@keycloakify/angular/lib/tokens/use-default-css'; 3 | import { ComponentReference } from '@keycloakify/angular/login/classes/component-reference'; 4 | import { KcClassDirective } from '@keycloakify/angular/login/directives/kc-class'; 5 | import type { I18n } from '@keycloakify/angular/login/i18n'; 6 | import type { KcContext } from '@keycloakify/angular/login/KcContext'; 7 | import { LOGIN_CLASSES } from '@keycloakify/angular/login/tokens/classes'; 8 | import { LOGIN_I18N } from '@keycloakify/angular/login/tokens/i18n'; 9 | import { KC_LOGIN_CONTEXT } from '@keycloakify/angular/login/tokens/kc-context'; 10 | import type { ClassKey } from 'keycloakify/login/lib/kcClsx'; 11 | 12 | @Component({ 13 | imports: [KcClassDirective], 14 | selector: 'kc-select-authenticator', 15 | templateUrl: 'select-authenticator.component.html', 16 | changeDetection: ChangeDetectionStrategy.OnPush, 17 | providers: [ 18 | { 19 | provide: ComponentReference, 20 | useExisting: forwardRef(() => SelectAuthenticatorComponent) 21 | } 22 | ] 23 | }) 24 | export class SelectAuthenticatorComponent extends ComponentReference { 25 | kcContext = inject>(KC_LOGIN_CONTEXT); 26 | i18n = inject(LOGIN_I18N); 27 | 28 | override doUseDefaultCss = inject(USE_DEFAULT_CSS); 29 | override classes = inject>>(LOGIN_CLASSES); 30 | 31 | documentTitle: string | undefined; 32 | bodyClassName: string | undefined; 33 | 34 | displayRequiredFields = false; 35 | displayInfo = false; 36 | displayMessage = true; 37 | 38 | headerNode = viewChild>('headerNode'); 39 | infoNode = viewChild>('infoNode'); 40 | socialProvidersNode = viewChild>('socialProvidersNode'); 41 | } 42 | --------------------------------------------------------------------------------