import {i18n} from './i18n.js'; import {html, css, LitElement} 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 'vpu-common/vpu-icon.js'; /** * 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.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 LitElement { constructor() { super(); this.lang = 'de'; this.forceLogin = false; this.loadPerson = false; this.clientId = ""; this.keyCloakInitCalled = false; this._keycloak = null; this.token = ""; this.subject = ""; this.name = ""; this.personId = ""; this.loggedIn = false; this.rememberLogin = false; this.person = null; // 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); } /** * See: https://lit-element.polymer-project.org/guide/properties#initialize */ static get properties() { return { lang: { type: String }, forceLogin: { type: Boolean, attribute: 'force-login' }, rememberLogin: { type: Boolean, attribute: 'remember-login' }, loggedIn: { type: Boolean}, loadPerson: { type: Boolean, attribute: 'load-person' }, clientId: { type: String, attribute: 'client-id' }, name: { type: String, attribute: false }, token: { type: String, attribute: false }, subject: { type: String, attribute: false }, personId: { type: String, attribute: false }, keycloak: { type: Object, attribute: false }, person: { type: Object, attribute: false }, }; } connectedCallback() { super.connectedCallback(); const href = window.location.href; if (this.rememberLogin && sessionStorage.getItem('vpu-logged-in')) { this.forceLogin = true; } // load Keycloak if we want to force the login or if we were redirected from the Keycloak login page if (this.forceLogin || (href.search('[&#]state=') >= 0 && href.search('[&#]session_state=') >= 0)) { this.loadKeycloak(); } this.updateComplete.then(()=>{ }); document.addEventListener('click', this.closeDropdown); } disconnectedCallback() { document.removeEventListener('click', this.closeDropdown); super.disconnectedCallback(); } loadKeycloak() { const that = this; console.log("loadKeycloak"); if (!this.keyCloakInitCalled) { // inject Keycloak javascript file const script = document.createElement('script'); script.type = 'text/javascript'; //script.async = true; script.onload = function () { that.keyCloakInitCalled = true; that._keycloak = Keycloak({ url: 'https://auth-dev.tugraz.at/auth', realm: 'tugraz', clientId: that.clientId, }); // See: https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter that._keycloak.init().success((authenticated) => { console.log(authenticated ? 'authenticated' : 'not authenticated!'); console.log(that._keycloak); if (!authenticated) { // set locale of Keycloak login page that._keycloak.login({kcLocale: that.lang}); return; } that.loggedIn = false; that.updateKeycloakData(); that.dispatchInitEvent(); if (that.loadPerson) { JSONLD.initialize(commonUtils.getAPiUrl(), (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.dispatchPersonInitEvent(); }); }, {}, that.lang); } }).error(function () { console.error('Keycloak failed to initialize!'); }); // auto-refresh token that._keycloak.onTokenExpired = function() { that._keycloak.updateToken(5).success(function(refreshed) { if (refreshed) { console.log('Token was successfully refreshed'); that.updateKeycloakData(); } else { console.log('Token is still valid'); } }).error(function() { console.log('Failed to refresh the token, or the session has expired'); }); } }; // https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_a script.src = '//auth-dev.tugraz.at/auth/js/keycloak.min.js'; //Append it to the document header document.head.appendChild(script); } } login(e) { this.loadKeycloak(); } logout(e) { sessionStorage.removeItem('vpu-logged-in'); this._keycloak.logout(); } /** * Dispatches the init event */ dispatchInitEvent() { this.loggedIn = true; this.dispatchEvent(this.initEvent); } /** * Dispatches the person init event */ dispatchPersonInitEvent() { this.dispatchEvent(this.personInitEvent); } /** * Dispatches the profile event */ dispatchProfileEvent() { this.dispatchEvent(this.profileEvent); } /** * Dispatches the keycloak data update event */ dispatchKeycloakDataUpdateEvent() { this.dispatchEvent(this.keycloakDataUpdateEvent); } updateKeycloakData() { this.name = this._keycloak.idTokenParsed.name; this.token = this._keycloak.token; this.subject = this._keycloak.subject; this.personId = this._keycloak.idTokenParsed.preferred_username; window.VPUAuthSubject = this.subject; window.VPUAuthToken = this.token; window.VPUUserFullName = this.name; window.VPUPersonId = this.personId; console.log("Bearer " + this.token); this.dispatchKeycloakDataUpdateEvent(); } update(changedProperties) { changedProperties.forEach((oldValue, propName) => { if (propName === "lang") { i18n.changeLanguage(this.lang); } if (propName == "loggedIn") { if (this.loggedIn) sessionStorage.setItem('vpu-logged-in', true); else sessionStorage.removeItem('vpu-logged-in'); } }); super.update(changedProperties); } static get styles() { // language=css return css` ${commonStyles.getThemeCSS()} :host { display: inline-block; } .dropdown.is-active .dropdown-menu, .dropdown.is-hoverable:hover .dropdown-menu { display: block; } .dropdown-menu { display: none; min-width: 12rem; padding-top: 4px; position: absolute; z-index: 20; } .dropdown-content { background-color: white; border-radius: var(--vpu-border-radius); box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1); padding-bottom: 0.5rem; padding-top: 0.5rem; } .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; } /* login button */ .loginbox svg path { fill: var(--vpu-primary-bg-color); } .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 { display: inline-block; } .loginbox .label { padding-left: 0.2em; } `; } onDropdownClick(event) { event.stopPropagation(); event.currentTarget.classList.toggle('is-active'); const chevron = this.shadowRoot.querySelector("#menu-chevron-icon"); if (chevron !== null) { chevron.name = event.currentTarget.classList.contains('is-active') ? 'chevron-up' : 'chevron-down'; } } closeDropdown() { var dropdowns = this.shadowRoot.querySelectorAll('.dropdown'); dropdowns.forEach(function (el) { el.classList.remove('is-active'); }); } renderLoggedIn() { const imageURL = (this.person && this.person.image) ? this.person.image : null; return html` <div class="dropdown" @click="${this.onDropdownClick}"> <div class="dropdown-trigger"> <span>${this.name}</span> <vpu-icon name="chevron-down" id="menu-chevron-icon"></vpu-icon> </div> <div class="dropdown-menu" id="dropdown-menu2" role="menu"> <div class="dropdown-content"> ${imageURL ? html`<img alt="" src="${imageURL}" width="40%" height="40%" class="dropdown-item">` : ''} <a href="#" @click="${(e) => {e.preventDefault(); this.dispatchProfileEvent();}}" class="dropdown-item">${i18n.t('profile')}</a> <a href="#" @click="${this.logout}" class="dropdown-item">${i18n.t('logout')}</a> </div> </div> </div> `; } renderLoggedOut() { let loginSVG = ` <svg viewBox="0 0 100 100" y="0px" x="0px" id="icon" 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` <div class="loginbox" @click="${this.login}"> <div class="icon">${unsafeHTML(loginSVG)}</div> <div class="label">${i18n.t('login')}</div> </div> `; } render() { commonUtils.initAssetBaseURL('vpu-auth-src'); return html` <div> ${this.loggedIn ? this.renderLoggedIn() : this.renderLoggedOut()} </div> `; } } commonUtils.defineCustomElement('vpu-auth', VPUAuth);