diff --git a/packages/auth/package.json b/packages/auth/package.json index 75f27bcf120fd5be3ec8ebf56ae05adab7e39220..895c39312ebe7e815a6383fabb895ff60a08a61f 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,7 +1,7 @@ { "name": "vpu-auth", "version": "1.0.0", - "main": "src/vpu-auth.js", + "main": "src/index.js", "devDependencies": { "chai": "^4.2.0", "glob": "^7.1.4", @@ -31,9 +31,10 @@ "vpu-common": "file:./vendor/common" }, "dependencies": { + "@open-wc/scoped-elements": "^1.0.8", + "event-target-shim": "^5.0.1", "keycloak-js": "^8.0.0", - "lit-element": "^2.1.0", - "event-target-shim": "^5.0.1" + "lit-element": "^2.1.0" }, "scripts": { "clean": "rm dist/*", diff --git a/packages/auth/src/auth.js b/packages/auth/src/auth.js new file mode 100644 index 0000000000000000000000000000000000000000..cce2cb7603fce273aa5f46d403ebe7f0052adefa --- /dev/null +++ b/packages/auth/src/auth.js @@ -0,0 +1,498 @@ +import {i18n} from './i18n.js'; +import {html, css} from 'lit-element'; +import {unsafeHTML} from 'lit-html/directives/unsafe-html.js'; +import JSONLD from 'vpu-common/jsonld'; +import * as commonUtils from 'vpu-common/utils'; +import * as commonStyles from 'vpu-common/styles'; +import * as events from 'vpu-common/events.js'; +import 'vpu-common/vpu-icon.js'; +import VPULitElement from 'vpu-common/vpu-lit-element'; +import {KeycloakWrapper} from './keycloak.js'; + + +const LoginStatus = Object.freeze({ + UNKNOWN: 'unknown', + LOGGING_IN: 'logging-in', + LOGGED_IN: 'logged-in', + LOGGING_OUT: 'logging-out', + LOGGED_OUT: 'logged-out', +}); + +/** + * Keycloak auth web component + * https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter + * + * Dispatches an event `vpu-auth-init` and sets some global variables: + * window.VPUAuthSubject: Keycloak username + * window.VPUAuthToken: Keycloak token to send with your requests + * window.VPUAuthTokenParsed: Keycloak token content + * window.VPUUserFullName: Full name of the user + * window.VPUPersonId: Person identifier of the user + * window.VPUPerson: Person json object of the user (optional, enable by setting the `load-person` attribute, + * which will dispatch a `vpu-auth-person-init` event when loaded) + */ +export class Auth extends VPULitElement { + constructor() { + super(); + this.lang = 'de'; + this.forceLogin = false; + this.loadPerson = false; + this.showProfile = false; + this.token = ""; + this.tokenParsed = null; + this.subject = ""; + this.name = ""; + this.personId = ""; + this.tryLogin = false; + this.person = null; + this.entryPointUrl = commonUtils.getAPiUrl(); + this.keycloakConfig = null; + + const _getLoginData = () => { + const message = { + status: this._loginStatus, + token: this.token, + }; + return message; + }; + + this._loginStatus = LoginStatus.UNKNOWN; + this._emitter = new events.EventEmitter('vpu-auth-update', 'vpu-auth-update-request'); + this._emitter.registerCallback(_getLoginData); + + // Create the events + this.initEvent = new CustomEvent("vpu-auth-init", { "detail": "KeyCloak init event", bubbles: true, composed: true }); + this.personInitEvent = new CustomEvent("vpu-auth-person-init", { "detail": "KeyCloak person init event", bubbles: true, composed: true }); + this.profileEvent = new CustomEvent("vpu-auth-profile", { "detail": "Profile event", bubbles: true, composed: true }); + this.keycloakDataUpdateEvent = new CustomEvent("vpu-auth-keycloak-data-update", { "detail": "KeyCloak data was updated", bubbles: true, composed: true }); + + this.closeDropdown = this.closeDropdown.bind(this); + this._onKCChanged = this._onKCChanged.bind(this); + } + + _onKCChanged(event) { + const kc = event.detail; + let newPerson = false; + + if (kc.authenticated) { + let tokenChanged = (this.token !== kc.token); + this.tokenParsed = kc.tokenParsed; + this.name = kc.idTokenParsed.name; + this.token = kc.token; + + this.subject = kc.subject; + const personId = kc.idTokenParsed.preferred_username; + if (personId !== this.personId) { + this.person = null; + newPerson = true; + } + this.personId = personId; + + window.VPUAuthSubject = this.subject; + window.VPUAuthToken = this.token; + window.VPUAuthTokenParsed = this.tokenParsed; + window.VPUUserFullName = this.name; + window.VPUPersonId = this.personId; + window.VPUPerson = this.person; + + this._setLoginStatus(LoginStatus.LOGGED_IN, tokenChanged); + } else { + if (this._loginStatus === LoginStatus.LOGGED_IN) { + this._setLoginStatus(LoginStatus.LOGGING_OUT); + } + this.name = ""; + this.token = ""; + this.tokenParsed = null; + this.subject = ""; + this.personId = ""; + this.person = null; + + window.VPUAuthSubject = this.subject; + window.VPUAuthToken = this.token; + window.VPUAuthTokenParsed = this.tokenParsed; + window.VPUUserFullName = this.name; + window.VPUPersonId = this.personId; + window.VPUPerson = this.person; + + this._setLoginStatus(LoginStatus.LOGGED_OUT); + } + + const that = this; + + if (newPerson) { + this.dispatchEvent(this.initEvent); + } + + if (newPerson && this.loadPerson) { + JSONLD.initialize(this.entryPointUrl, (jsonld) => { + // find the correct api url for the current person + // we are fetching the logged-in person directly to respect the REST philosophy + // see: https://github.com/api-platform/api-platform/issues/337 + const apiUrl = jsonld.getApiUrlForEntityName("Person") + '/' + that.personId; + + fetch(apiUrl, { + headers: { + 'Content-Type': 'application/ld+json', + 'Authorization': 'Bearer ' + that.token, + }, + }) + .then(response => response.json()) + .then((person) => { + that.person = person; + window.VPUPerson = person; + that.dispatchEvent(that.personInitEvent); + this._setLoginStatus(this._loginStatus, true); + }); + }, {}, that.lang); + } + + this.dispatchEvent(this.keycloakDataUpdateEvent); + } + + _setLoginStatus(status, force) { + if (this._loginStatus === status && !force) + return; + + this._loginStatus = status; + this._emitter.emit(); + } + + /** + * See: https://lit-element.polymer-project.org/guide/properties#initialize + */ + static get properties() { + return { + lang: { type: String }, + forceLogin: { type: Boolean, attribute: 'force-login' }, + tryLogin: { type: Boolean, attribute: 'try-login' }, + loadPerson: { type: Boolean, attribute: 'load-person' }, + showProfile: { type: Boolean, attribute: 'show-profile' }, + entryPointUrl: { type: String, attribute: 'entry-point-url' }, + keycloakConfig: { type: Object, attribute: 'keycloak-config' }, + name: { type: String, attribute: false }, + token: { type: String, attribute: false }, + subject: { type: String, attribute: false }, + personId: { type: String, attribute: false }, + person: { type: Object, attribute: false }, + _loginStatus: { type: String, attribute: false }, + }; + } + + _getScope() { + if (this.keycloakConfig !== null) { + return this.keycloakConfig.scope || ""; + } + return ""; + } + + connectedCallback() { + super.connectedCallback(); + + // Keycloak config + let baseURL = commonUtils.setting('keyCloakBaseURL'); + let realm = commonUtils.setting('keyCloakRealm'); + let clientId = commonUtils.setting('keyCloakClientId'); + let silentCheckSsoRedirectUri = ''; + if (this.keycloakConfig !== null) { + baseURL = this.keycloakConfig.url || baseURL; + realm = this.keycloakConfig.realm || realm; + clientId = this.keycloakConfig.clientId || clientId; + silentCheckSsoRedirectUri = this.keycloakConfig.silentCheckSsoRedirectUri || silentCheckSsoRedirectUri; + } + if (!baseURL || !realm || !clientId) { + throw Error("Keycloak config not set"); + } + + this._kcwrapper = new KeycloakWrapper(baseURL, realm, clientId, silentCheckSsoRedirectUri); + this._kcwrapper.addEventListener('changed', this._onKCChanged); + + const handleLogin = async () => { + if (this.forceLogin || this._kcwrapper.isLoggingIn()) { + this._setLoginStatus(LoginStatus.LOGGING_IN); + await this._kcwrapper.login({lang: this.lang, scope: this._getScope()}); + } else if (this.tryLogin) { + this._setLoginStatus(LoginStatus.LOGGING_IN); + await this._kcwrapper.tryLogin(); + if (this._loginStatus === LoginStatus.LOGGING_IN) + this._setLoginStatus(LoginStatus.LOGGED_OUT); + } else { + this._setLoginStatus(LoginStatus.LOGGED_OUT); + } + }; + + handleLogin(); + + this.updateComplete.then(() => { + window.onresize = () => { + this.updateDropdownWidth(); + }; + }); + + document.addEventListener('click', this.closeDropdown); + } + + /** + * Set the dropdown width to almost the width of the web component + * We need to set the width manually because a percent width is in relation to the viewport + */ + updateDropdownWidth() { + const dropdown = this._("div.dropdown-menu"); + + if (!dropdown) { + return; + } + + dropdown.setAttribute("style", `width: ${this.offsetWidth - 5}px`); + } + + disconnectedCallback() { + this._kcwrapper.removeEventListener('changed', this._onKCChanged); + document.removeEventListener('click', this.closeDropdown); + super.disconnectedCallback(); + } + + onLoginClicked(e) { + this._kcwrapper.login({lang: this.lang, scope: this._getScope()}); + e.preventDefault(); + } + + onLogoutClicked(e) { + // Keycloak will redirect right away without emitting events, so we have + // to do this manually here + if (this._loginStatus === LoginStatus.LOGGED_IN) { + this._setLoginStatus(LoginStatus.LOGGING_OUT); + } + this._kcwrapper.logout(); + } + + update(changedProperties) { + changedProperties.forEach((oldValue, propName) => { + if (propName === "lang") { + i18n.changeLanguage(this.lang); + } + }); + + super.update(changedProperties); + } + + static get styles() { + // language=css + return css` + ${commonStyles.getThemeCSS()} + + :host { + display: inline-block; + } + + a { + color: currentColor; + cursor: pointer; + text-decoration: none; + } + + img { + border-width: var(--vpu-border-width); + border-color: var(--vpu-dark); + border-style: solid; + } + + .dropdown.is-active .dropdown-menu, .dropdown.is-hoverable:hover .dropdown-menu { + display: block; + } + + .dropdown-menu { + display: none; + min-width: 8em; + padding-top: 4px; + position: absolute; + z-index: 20; + border: solid 1px black; + border-radius: var(--vpu-border-radius); + overflow: hidden; + background-color: white; + } + + .dropdown-content { + background-color: white; + padding-bottom: 0.5rem; + padding-top: 0.5rem; + } + + .dropdown-content img { + max-width: 120px; + } + + .menu a { + /*padding: 0.3em;*/ + font-weight: 400; + color: #000; + display: block; + text-decoration: none; + } + + .menu a:hover { + color: #E4154B; + } + + .menu a.selected { color: white; background-color: black; } + + .dropdown-item { + color: #4a4a4a; + display: block; + font-size: 0.875rem; + line-height: 1.5; + padding: 0.375rem 1rem; + position: relative; + } + + .dropdown, img.login { + cursor: pointer; + } + + a.dropdown-item { + width: initial !important; + } + + .main-button { + min-width: 150px; + } + + .dropdown-trigger { + white-space: nowrap; + } + + vpu-icon { + height: 1em; + width: 1em; + vertical-align: -0.1rem; + } + + .loginbox svg { + width: 1.2em; + height: 1.2em; + top: 0.15em; + position: relative; + } + + .loginbox { + display: flex; + + align-items: center; + padding: 0 0.1em; + transition: background-color 0.15s, color 0.15s; + } + + .loginbox:hover { + background-color: var(--vpu-dark); + color: var(--vpu-light); + cursor: pointer; + transition: none; + } + + .loginbox:hover svg path { + fill: var(--vpu-light); + } + + .loginbox .icon, .authbox { + display: inline-block; + } + + .loginbox .label { + padding-left: 0.2em; + } + `; + } + + setChevron(name) { + const chevron = this.shadowRoot.querySelector("#menu-chevron-icon"); + if (chevron !== null) { + chevron.name = name; + } + } + + onDropdownClick(event) { + event.stopPropagation(); + event.currentTarget.classList.toggle('is-active'); + this.setChevron(event.currentTarget.classList.contains('is-active') ? 'chevron-up' : 'chevron-down'); + this.updateDropdownWidth(); + } + + closeDropdown() { + var dropdowns = this.shadowRoot.querySelectorAll('.dropdown'); + dropdowns.forEach(function (el) { + el.classList.remove('is-active'); + }); + this.setChevron('chevron-down'); + } + + onProfileClicked(event) { + event.preventDefault(); + this.dispatchEvent(this.profileEvent); + } + + renderLoggedIn() { + const imageURL = (this.person && this.person.image) ? this.person.image : null; + + return html` + <div class="dropdown" @click="${this.onDropdownClick}"> + <div class="dropdown-trigger"> + <a href="#"> + <span>${this.name}</span> + <vpu-icon name="chevron-down" id="menu-chevron-icon"></vpu-icon> + </a> + </div> + <div class="dropdown-menu" id="dropdown-menu2" role="menu"> + <div class="dropdown-content" @blur="${this.closeDropdown}"> + ${imageURL ? html`<div class="dropdown-item"><img alt="" src="${imageURL}"></div>` : ''} + <div class="menu"> + ${this.showProfile ? html`<a href="#" @click="${this.onProfileClicked}" class="dropdown-item">${i18n.t('profile')}</a>` :''} + <a href="#" @click="${this.onLogoutClicked}" class="dropdown-item">${i18n.t('logout')}</a> + </div> + </div> + </div> + </div> + `; + } + + renderLoggedOut() { + let loginSVG = ` + <svg + viewBox="0 0 100 100" + y="0px" + x="0px" + id="icon" + role="img" + version="1.1"> + <g + id="g6"> + <path + style="stroke-width:1.33417916" + id="path2" + d="m 42.943908,38.894934 5.885859,6.967885 H 5.4215537 c -1.8393311,0 -3.4334181,1.741972 -3.4334181,4.064599 0,2.322628 1.4714649,4.064599 3.4334181,4.064599 H 48.829767 L 42.943908,60.9599 c -1.348843,1.596808 -1.348843,4.064599 0,5.661406 1.348843,1.596808 3.433418,1.596808 4.782261,0 L 61.705085,49.927418 47.726169,33.378693 c -1.348843,-1.596806 -3.433418,-1.596806 -4.782261,0 -1.348843,1.596807 -1.348843,4.064599 0,5.516241 z" /> + <path + id="path4" + d="m 50,2.3007812 c -18.777325,0 -35.049449,10.9124408 -42.8261719,26.7246098 H 13.390625 C 20.672112,16.348362 34.336876,7.8007812 50,7.8007812 73.3,7.8007812 92.300781,26.7 92.300781,50 92.300781,73.3 73.3,92.300781 50,92.300781 c -15.673389,0 -29.345175,-8.60579 -36.623047,-21.326172 H 7.1640625 C 14.942553,86.8272 31.242598,97.800781 50.099609,97.800781 76.399609,97.800781 97.900391,76.4 97.900391,50 97.800391,23.7 76.3,2.3007812 50,2.3007812 Z" /> + </g> + </svg> + `; + + return html` + <a href="#" @click="${this.onLoginClicked}"> + <div class="loginbox"> + <div class="icon">${unsafeHTML(loginSVG)}</div> + <div class="label">${i18n.t('login')}</div> + </div> + </a> + `; + } + + render() { + commonUtils.initAssetBaseURL('vpu-auth-src'); + const loggedIn = (this._loginStatus === LoginStatus.LOGGED_IN); + return html` + <div class="authbox"> + ${loggedIn ? this.renderLoggedIn() : this.renderLoggedOut()} + </div> + `; + } +} \ No newline at end of file diff --git a/packages/auth/src/index.js b/packages/auth/src/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f8748296ced90ddf40eb0a1987820ff58a4fd48d --- /dev/null +++ b/packages/auth/src/index.js @@ -0,0 +1,3 @@ +import {Auth} from './auth.js'; + +export {Auth}; \ No newline at end of file diff --git a/packages/auth/src/vpu-auth-demo.js b/packages/auth/src/vpu-auth-demo.js index a6dfb89d8b0adce9a5fb340ecf59a85d46ccf9cd..e30722e16d17eaf863b2f1575976393b779ec49f 100644 --- a/packages/auth/src/vpu-auth-demo.js +++ b/packages/auth/src/vpu-auth-demo.js @@ -1,14 +1,21 @@ import {i18n} from './i18n.js'; import {html, LitElement} from 'lit-element'; -import './vpu-auth'; +import {ScopedElementsMixin} from '@open-wc/scoped-elements'; +import {Auth} from './auth.js'; import * as commonUtils from 'vpu-common/utils'; -class AuthDemo extends LitElement { +class AuthDemo extends ScopedElementsMixin(LitElement) { constructor() { super(); this.lang = 'de'; } + static get scopedElements() { + return { + 'vpu-auth': Auth, + }; + } + static get properties() { return { lang: { type: String }, diff --git a/packages/auth/src/vpu-auth.js b/packages/auth/src/vpu-auth.js index b150f73d94333ae8ca34e803ec248e5eb8c6c653..c6be62533612ba7eca05fcd1bbd9aabe4d05a970 100644 --- a/packages/auth/src/vpu-auth.js +++ b/packages/auth/src/vpu-auth.js @@ -1,500 +1,4 @@ -import {i18n} from './i18n.js'; -import {html, css} from 'lit-element'; -import {unsafeHTML} from 'lit-html/directives/unsafe-html.js'; -import JSONLD from 'vpu-common/jsonld'; import * as commonUtils from 'vpu-common/utils'; -import * as commonStyles from 'vpu-common/styles'; -import * as events from 'vpu-common/events.js'; -import 'vpu-common/vpu-icon.js'; -import VPULitElement from 'vpu-common/vpu-lit-element'; -import {KeycloakWrapper} from './keycloak.js'; +import {Auth} from './auth.js'; - -const LoginStatus = Object.freeze({ - UNKNOWN: 'unknown', - LOGGING_IN: 'logging-in', - LOGGED_IN: 'logged-in', - LOGGING_OUT: 'logging-out', - LOGGED_OUT: 'logged-out', -}); - -/** - * Keycloak auth web component - * https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter - * - * Dispatches an event `vpu-auth-init` and sets some global variables: - * window.VPUAuthSubject: Keycloak username - * window.VPUAuthToken: Keycloak token to send with your requests - * window.VPUAuthTokenParsed: Keycloak token content - * window.VPUUserFullName: Full name of the user - * window.VPUPersonId: Person identifier of the user - * window.VPUPerson: Person json object of the user (optional, enable by setting the `load-person` attribute, - * which will dispatch a `vpu-auth-person-init` event when loaded) - */ -class VPUAuth extends VPULitElement { - constructor() { - super(); - this.lang = 'de'; - this.forceLogin = false; - this.loadPerson = false; - this.showProfile = false; - this.token = ""; - this.tokenParsed = null; - this.subject = ""; - this.name = ""; - this.personId = ""; - this.tryLogin = false; - this.person = null; - this.entryPointUrl = commonUtils.getAPiUrl(); - this.keycloakConfig = null; - - const _getLoginData = () => { - const message = { - status: this._loginStatus, - token: this.token, - }; - return message; - }; - - this._loginStatus = LoginStatus.UNKNOWN; - this._emitter = new events.EventEmitter('vpu-auth-update', 'vpu-auth-update-request'); - this._emitter.registerCallback(_getLoginData); - - // Create the events - this.initEvent = new CustomEvent("vpu-auth-init", { "detail": "KeyCloak init event", bubbles: true, composed: true }); - this.personInitEvent = new CustomEvent("vpu-auth-person-init", { "detail": "KeyCloak person init event", bubbles: true, composed: true }); - this.profileEvent = new CustomEvent("vpu-auth-profile", { "detail": "Profile event", bubbles: true, composed: true }); - this.keycloakDataUpdateEvent = new CustomEvent("vpu-auth-keycloak-data-update", { "detail": "KeyCloak data was updated", bubbles: true, composed: true }); - - this.closeDropdown = this.closeDropdown.bind(this); - this._onKCChanged = this._onKCChanged.bind(this); - } - - _onKCChanged(event) { - const kc = event.detail; - let newPerson = false; - - if (kc.authenticated) { - let tokenChanged = (this.token !== kc.token); - this.tokenParsed = kc.tokenParsed; - this.name = kc.idTokenParsed.name; - this.token = kc.token; - - this.subject = kc.subject; - const personId = kc.idTokenParsed.preferred_username; - if (personId !== this.personId) { - this.person = null; - newPerson = true; - } - this.personId = personId; - - window.VPUAuthSubject = this.subject; - window.VPUAuthToken = this.token; - window.VPUAuthTokenParsed = this.tokenParsed; - window.VPUUserFullName = this.name; - window.VPUPersonId = this.personId; - window.VPUPerson = this.person; - - this._setLoginStatus(LoginStatus.LOGGED_IN, tokenChanged); - } else { - if (this._loginStatus === LoginStatus.LOGGED_IN) { - this._setLoginStatus(LoginStatus.LOGGING_OUT); - } - this.name = ""; - this.token = ""; - this.tokenParsed = null; - this.subject = ""; - this.personId = ""; - this.person = null; - - window.VPUAuthSubject = this.subject; - window.VPUAuthToken = this.token; - window.VPUAuthTokenParsed = this.tokenParsed; - window.VPUUserFullName = this.name; - window.VPUPersonId = this.personId; - window.VPUPerson = this.person; - - this._setLoginStatus(LoginStatus.LOGGED_OUT); - } - - const that = this; - - if (newPerson) { - this.dispatchEvent(this.initEvent); - } - - if (newPerson && this.loadPerson) { - JSONLD.initialize(this.entryPointUrl, (jsonld) => { - // find the correct api url for the current person - // we are fetching the logged-in person directly to respect the REST philosophy - // see: https://github.com/api-platform/api-platform/issues/337 - const apiUrl = jsonld.getApiUrlForEntityName("Person") + '/' + that.personId; - - fetch(apiUrl, { - headers: { - 'Content-Type': 'application/ld+json', - 'Authorization': 'Bearer ' + that.token, - }, - }) - .then(response => response.json()) - .then((person) => { - that.person = person; - window.VPUPerson = person; - that.dispatchEvent(that.personInitEvent); - this._setLoginStatus(this._loginStatus, true); - }); - }, {}, that.lang); - } - - this.dispatchEvent(this.keycloakDataUpdateEvent); - } - - _setLoginStatus(status, force) { - if (this._loginStatus === status && !force) - return; - - this._loginStatus = status; - this._emitter.emit(); - } - - /** - * See: https://lit-element.polymer-project.org/guide/properties#initialize - */ - static get properties() { - return { - lang: { type: String }, - forceLogin: { type: Boolean, attribute: 'force-login' }, - tryLogin: { type: Boolean, attribute: 'try-login' }, - loadPerson: { type: Boolean, attribute: 'load-person' }, - showProfile: { type: Boolean, attribute: 'show-profile' }, - entryPointUrl: { type: String, attribute: 'entry-point-url' }, - keycloakConfig: { type: Object, attribute: 'keycloak-config' }, - name: { type: String, attribute: false }, - token: { type: String, attribute: false }, - subject: { type: String, attribute: false }, - personId: { type: String, attribute: false }, - person: { type: Object, attribute: false }, - _loginStatus: { type: String, attribute: false }, - }; - } - - _getScope() { - if (this.keycloakConfig !== null) { - return this.keycloakConfig.scope || ""; - } - return ""; - } - - connectedCallback() { - super.connectedCallback(); - - // Keycloak config - let baseURL = commonUtils.setting('keyCloakBaseURL'); - let realm = commonUtils.setting('keyCloakRealm'); - let clientId = commonUtils.setting('keyCloakClientId'); - let silentCheckSsoRedirectUri = ''; - if (this.keycloakConfig !== null) { - baseURL = this.keycloakConfig.url || baseURL; - realm = this.keycloakConfig.realm || realm; - clientId = this.keycloakConfig.clientId || clientId; - silentCheckSsoRedirectUri = this.keycloakConfig.silentCheckSsoRedirectUri || silentCheckSsoRedirectUri; - } - if (!baseURL || !realm || !clientId) { - throw Error("Keycloak config not set"); - } - - this._kcwrapper = new KeycloakWrapper(baseURL, realm, clientId, silentCheckSsoRedirectUri); - this._kcwrapper.addEventListener('changed', this._onKCChanged); - - const handleLogin = async () => { - if (this.forceLogin || this._kcwrapper.isLoggingIn()) { - this._setLoginStatus(LoginStatus.LOGGING_IN); - await this._kcwrapper.login({lang: this.lang, scope: this._getScope()}); - } else if (this.tryLogin) { - this._setLoginStatus(LoginStatus.LOGGING_IN); - await this._kcwrapper.tryLogin(); - if (this._loginStatus === LoginStatus.LOGGING_IN) - this._setLoginStatus(LoginStatus.LOGGED_OUT); - } else { - this._setLoginStatus(LoginStatus.LOGGED_OUT); - } - }; - - handleLogin(); - - this.updateComplete.then(() => { - window.onresize = () => { - this.updateDropdownWidth(); - }; - }); - - document.addEventListener('click', this.closeDropdown); - } - - /** - * Set the dropdown width to almost the width of the web component - * We need to set the width manually because a percent width is in relation to the viewport - */ - updateDropdownWidth() { - const dropdown = this._("div.dropdown-menu"); - - if (!dropdown) { - return; - } - - dropdown.setAttribute("style", `width: ${this.offsetWidth - 5}px`); - } - - disconnectedCallback() { - this._kcwrapper.removeEventListener('changed', this._onKCChanged); - document.removeEventListener('click', this.closeDropdown); - super.disconnectedCallback(); - } - - onLoginClicked(e) { - this._kcwrapper.login({lang: this.lang, scope: this._getScope()}); - e.preventDefault(); - } - - onLogoutClicked(e) { - // Keycloak will redirect right away without emitting events, so we have - // to do this manually here - if (this._loginStatus === LoginStatus.LOGGED_IN) { - this._setLoginStatus(LoginStatus.LOGGING_OUT); - } - this._kcwrapper.logout(); - } - - update(changedProperties) { - changedProperties.forEach((oldValue, propName) => { - if (propName === "lang") { - i18n.changeLanguage(this.lang); - } - }); - - super.update(changedProperties); - } - - static get styles() { - // language=css - return css` - ${commonStyles.getThemeCSS()} - - :host { - display: inline-block; - } - - a { - color: currentColor; - cursor: pointer; - text-decoration: none; - } - - img { - border-width: var(--vpu-border-width); - border-color: var(--vpu-dark); - border-style: solid; - } - - .dropdown.is-active .dropdown-menu, .dropdown.is-hoverable:hover .dropdown-menu { - display: block; - } - - .dropdown-menu { - display: none; - min-width: 8em; - padding-top: 4px; - position: absolute; - z-index: 20; - border: solid 1px black; - border-radius: var(--vpu-border-radius); - overflow: hidden; - background-color: white; - } - - .dropdown-content { - background-color: white; - padding-bottom: 0.5rem; - padding-top: 0.5rem; - } - - .dropdown-content img { - max-width: 120px; - } - - .menu a { - /*padding: 0.3em;*/ - font-weight: 400; - color: #000; - display: block; - text-decoration: none; - } - - .menu a:hover { - color: #E4154B; - } - - .menu a.selected { color: white; background-color: black; } - - .dropdown-item { - color: #4a4a4a; - display: block; - font-size: 0.875rem; - line-height: 1.5; - padding: 0.375rem 1rem; - position: relative; - } - - .dropdown, img.login { - cursor: pointer; - } - - a.dropdown-item { - width: initial !important; - } - - .main-button { - min-width: 150px; - } - - .dropdown-trigger { - white-space: nowrap; - } - - vpu-icon { - height: 1em; - width: 1em; - vertical-align: -0.1rem; - } - - .loginbox svg { - width: 1.2em; - height: 1.2em; - top: 0.15em; - position: relative; - } - - .loginbox { - display: flex; - - align-items: center; - padding: 0 0.1em; - transition: background-color 0.15s, color 0.15s; - } - - .loginbox:hover { - background-color: var(--vpu-dark); - color: var(--vpu-light); - cursor: pointer; - transition: none; - } - - .loginbox:hover svg path { - fill: var(--vpu-light); - } - - .loginbox .icon, .authbox { - display: inline-block; - } - - .loginbox .label { - padding-left: 0.2em; - } - `; - } - - setChevron(name) { - const chevron = this.shadowRoot.querySelector("#menu-chevron-icon"); - if (chevron !== null) { - chevron.name = name; - } - } - - onDropdownClick(event) { - event.stopPropagation(); - event.currentTarget.classList.toggle('is-active'); - this.setChevron(event.currentTarget.classList.contains('is-active') ? 'chevron-up' : 'chevron-down'); - this.updateDropdownWidth(); - } - - closeDropdown() { - var dropdowns = this.shadowRoot.querySelectorAll('.dropdown'); - dropdowns.forEach(function (el) { - el.classList.remove('is-active'); - }); - this.setChevron('chevron-down'); - } - - onProfileClicked(event) { - event.preventDefault(); - this.dispatchEvent(this.profileEvent); - } - - renderLoggedIn() { - const imageURL = (this.person && this.person.image) ? this.person.image : null; - - return html` - <div class="dropdown" @click="${this.onDropdownClick}"> - <div class="dropdown-trigger"> - <a href="#"> - <span>${this.name}</span> - <vpu-icon name="chevron-down" id="menu-chevron-icon"></vpu-icon> - </a> - </div> - <div class="dropdown-menu" id="dropdown-menu2" role="menu"> - <div class="dropdown-content" @blur="${this.closeDropdown}"> - ${imageURL ? html`<div class="dropdown-item"><img alt="" src="${imageURL}"></div>` : ''} - <div class="menu"> - ${this.showProfile ? html`<a href="#" @click="${this.onProfileClicked}" class="dropdown-item">${i18n.t('profile')}</a>` :''} - <a href="#" @click="${this.onLogoutClicked}" class="dropdown-item">${i18n.t('logout')}</a> - </div> - </div> - </div> - </div> - `; - } - - renderLoggedOut() { - let loginSVG = ` - <svg - viewBox="0 0 100 100" - y="0px" - x="0px" - id="icon" - role="img" - version="1.1"> - <g - id="g6"> - <path - style="stroke-width:1.33417916" - id="path2" - d="m 42.943908,38.894934 5.885859,6.967885 H 5.4215537 c -1.8393311,0 -3.4334181,1.741972 -3.4334181,4.064599 0,2.322628 1.4714649,4.064599 3.4334181,4.064599 H 48.829767 L 42.943908,60.9599 c -1.348843,1.596808 -1.348843,4.064599 0,5.661406 1.348843,1.596808 3.433418,1.596808 4.782261,0 L 61.705085,49.927418 47.726169,33.378693 c -1.348843,-1.596806 -3.433418,-1.596806 -4.782261,0 -1.348843,1.596807 -1.348843,4.064599 0,5.516241 z" /> - <path - id="path4" - d="m 50,2.3007812 c -18.777325,0 -35.049449,10.9124408 -42.8261719,26.7246098 H 13.390625 C 20.672112,16.348362 34.336876,7.8007812 50,7.8007812 73.3,7.8007812 92.300781,26.7 92.300781,50 92.300781,73.3 73.3,92.300781 50,92.300781 c -15.673389,0 -29.345175,-8.60579 -36.623047,-21.326172 H 7.1640625 C 14.942553,86.8272 31.242598,97.800781 50.099609,97.800781 76.399609,97.800781 97.900391,76.4 97.900391,50 97.800391,23.7 76.3,2.3007812 50,2.3007812 Z" /> - </g> - </svg> - `; - - return html` - <a href="#" @click="${this.onLoginClicked}"> - <div class="loginbox"> - <div class="icon">${unsafeHTML(loginSVG)}</div> - <div class="label">${i18n.t('login')}</div> - </div> - </a> - `; - } - - render() { - commonUtils.initAssetBaseURL('vpu-auth-src'); - const loggedIn = (this._loginStatus === LoginStatus.LOGGED_IN); - return html` - <div class="authbox"> - ${loggedIn ? this.renderLoggedIn() : this.renderLoggedOut()} - </div> - `; - } -} - -commonUtils.defineCustomElement('vpu-auth', VPUAuth); +commonUtils.defineCustomElement('vpu-auth', Auth);