Skip to content
Snippets Groups Projects
Commit 379a193f authored by Reiter, Christoph's avatar Reiter, Christoph :snake:
Browse files

Split vpu-auth into vpu-auth and vpu-auth-button

vpu-auth listens to login/logout events which the button emits
and the button shows data from the auth-update event.
parent c060ba2a
No related branches found
No related tags found
No related merge requests found
import {i18n} from './i18n.js';
import {html, css} from 'lit-element';
import {unsafeHTML} from 'lit-html/directives/unsafe-html.js';
import {ScopedElementsMixin} from '@open-wc/scoped-elements';
import * as commonStyles from 'vpu-common/styles';
import {LitElement} from "lit-element";
import {Icon, EventBus} from 'vpu-common';
import {LoginStatus} from './util.js';
export class AuthButton extends ScopedElementsMixin(LitElement) {
constructor() {
super();
this.lang = 'de';
this.showProfile = false;
this.showImage = false;
this._loginData = {};
this.closeDropdown = this.closeDropdown.bind(this);
this.onWindowResize = this.onWindowResize.bind(this);
}
static get scopedElements() {
return {
'vpu-icon': Icon,
};
}
static get properties() {
return {
lang: { type: String },
showProfile: { type: Boolean, attribute: 'show-profile' },
showImage: { type: Boolean, attribute: 'show-image' },
_loginData: { type: Object, attribute: false },
};
}
onWindowResize() {
this.updateDropdownWidth();
}
connectedCallback() {
super.connectedCallback();
this._bus = new EventBus();
this._bus.subscribe('auth-update', (data) => {
this._loginData = data;
});
window.addEventListener('resize', this.onWindowResize);
document.addEventListener('click', this.closeDropdown);
}
disconnectedCallback() {
window.removeEventListener('resize', this.onWindowResize);
this._bus.close();
document.removeEventListener('click', this.closeDropdown);
super.disconnectedCallback();
}
/**
* 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.shadowRoot.querySelector("div.dropdown-menu");
if (!dropdown) {
return;
}
let viewportOffset = this.getBoundingClientRect();
let spaceToRIght = window.innerWidth - viewportOffset.left;
dropdown.setAttribute("style", `width: ${spaceToRIght - 20}px`);
}
onLoginClicked(e) {
this._bus.publish('auth-login');
e.preventDefault();
}
onLogoutClicked(e) {
this._bus.publish('auth-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: 5em;
max-width: 25em;
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;
}
.menu-icon {
height: 1em;
width: 1em;
vertical-align: -0.1rem;
}
.login-box svg {
width: 1.1em;
height: 1.1em;
display: flex;
}
.login-button {
padding: 0.3em 0.4em;
transition: background-color 0.15s, color 0.15s;
}
.login-button:hover {
background-color: var(--vpu-dark);
color: var(--vpu-light);
cursor: pointer;
transition: none;
}
.login-box {
display: flex;
align-items: center;
}
.login-box:hover svg path {
fill: var(--vpu-light);
}
.login-box .label {
padding-left: 0.2em;
}
.dropdown-trigger {
display: flex;
align-items: center;
}
.dropdown-trigger .name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
margin-right: 0.5em
}
`;
}
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();
const profileEvent = new CustomEvent("vpu-auth-profile", {
"detail": "Profile event",
bubbles: true,
composed: true,
});
this.dispatchEvent(profileEvent);
}
renderLoggedIn() {
const person = this._loginData.person;
const imageURL = (this.showImage && person && person.image) ? person.image : null;
return html`
<div class="dropdown" @click="${this.onDropdownClick}">
<a href="#">
<div class="dropdown-trigger login-button">
<div class="name">${this._loginData.name}</div>
<vpu-icon class="menu-icon" name="chevron-down" id="menu-chevron-icon"></vpu-icon>
</div>
</a>
<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="login-box login-button">
<div class="icon">${unsafeHTML(loginSVG)}</div>
<div class="label">${i18n.t('login')}</div>
</div>
</a>
`;
}
render() {
const loggedIn = (this._loginData.status === LoginStatus.LOGGED_IN);
return html`
<div class="authbox">
${loggedIn ? this.renderLoggedIn() : this.renderLoggedOut()}
</div>
`;
}
}
\ No newline at end of file
import {i18n} from './i18n.js';
import {html, css} from 'lit-element';
import {unsafeHTML} from 'lit-html/directives/unsafe-html.js';
import {ScopedElementsMixin} from '@open-wc/scoped-elements';
import JSONLD from 'vpu-common/jsonld';
import * as commonUtils from 'vpu-common/utils';
import * as commonStyles from 'vpu-common/styles';
import {Icon, EventBus} from 'vpu-common';
import VPULitElement from 'vpu-common/vpu-lit-element';
import {EventBus} from 'vpu-common';
import {KeycloakWrapper} from './keycloak.js';
import {LitElement} from "lit-element";
import {LoginStatus} from './util.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
......@@ -31,14 +20,12 @@ const LoginStatus = Object.freeze({
* 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 ScopedElementsMixin(VPULitElement) {
export class Auth extends LitElement {
constructor() {
super();
this.lang = 'de';
this.forceLogin = false;
this.loadPerson = false;
this.showProfile = false;
this.showImage = false;
this.token = "";
this.tokenParsed = null;
this.subject = "";
......@@ -53,17 +40,19 @@ export class Auth extends ScopedElementsMixin(VPULitElement) {
// 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);
}
}
static get scopedElements() {
return {
'vpu-icon': Icon,
};
update(changedProperties) {
changedProperties.forEach((oldValue, propName) => {
if (propName === "lang") {
i18n.changeLanguage(this.lang);
}
});
super.update(changedProperties);
}
_onKCChanged(event) {
......@@ -91,7 +80,7 @@ export class Auth extends ScopedElementsMixin(VPULitElement) {
window.VPUPersonId = this.personId;
window.VPUPerson = this.person;
this._setLoginStatus(LoginStatus.LOGGED_IN, tokenChanged);
this._setLoginStatus(LoginStatus.LOGGED_IN, tokenChanged || newPerson);
} else {
if (this._loginStatus === LoginStatus.LOGGED_IN) {
this._setLoginStatus(LoginStatus.LOGGING_OUT);
......@@ -154,22 +143,19 @@ export class Auth extends ScopedElementsMixin(VPULitElement) {
this._bus.publish('auth-update', {
status: this._loginStatus,
token: this.token,
name: this.name,
person: this.person,
}, {
retain: true,
});
}
/**
* 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' },
showImage: { type: Boolean, attribute: 'show-image' },
entryPointUrl: { type: String, attribute: 'entry-point-url' },
keycloakConfig: { type: Object, attribute: 'keycloak-config' },
name: { type: String, attribute: false },
......@@ -211,6 +197,26 @@ export class Auth extends ScopedElementsMixin(VPULitElement) {
this._kcwrapper = new KeycloakWrapper(baseURL, realm, clientId, silentCheckSsoRedirectUri);
this._kcwrapper.addEventListener('changed', this._onKCChanged);
this._bus.subscribe('auth-login', () => {
this._kcwrapper.login({lang: this.lang, scope: this._getScope()});
});
this._bus.subscribe('auth-logout', () => {
// 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();
// In case logout was aborted, for example with beforeunload,
// revert back to being logged in
if (this._loginStatus === LoginStatus.LOGGING_OUT) {
this._setLoginStatus(LoginStatus.LOGGED_IN);
}
});
const handleLogin = async () => {
if (this.forceLogin || this._kcwrapper.isLoggingIn()) {
this._setLoginStatus(LoginStatus.LOGGING_IN);
......@@ -226,291 +232,12 @@ export class Auth extends ScopedElementsMixin(VPULitElement) {
};
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;
}
let viewportOffset = this.getBoundingClientRect();
let spaceToRIght = window.innerWidth - viewportOffset.left;
dropdown.setAttribute("style", `width: ${spaceToRIght - 20}px`);
}
disconnectedCallback() {
this._bus.close();
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();
// In case logout was aborted, for example with beforeunload,
// revert back to being logged in
if (this._loginStatus === LoginStatus.LOGGING_OUT) {
this._setLoginStatus(LoginStatus.LOGGED_IN);
}
}
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: 5em;
max-width: 25em;
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;
}
.menu-icon {
height: 1em;
width: 1em;
vertical-align: -0.1rem;
}
.login-box svg {
width: 1.1em;
height: 1.1em;
display: flex;
}
.login-button {
padding: 0.3em 0.4em;
transition: background-color 0.15s, color 0.15s;
}
.login-button:hover {
background-color: var(--vpu-dark);
color: var(--vpu-light);
cursor: pointer;
transition: none;
}
.login-box {
display: flex;
align-items: center;
}
.login-box:hover svg path {
fill: var(--vpu-light);
}
.login-box .label {
padding-left: 0.2em;
}
.dropdown-trigger {
display: flex;
align-items: center;
}
.dropdown-trigger .name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
margin-right: 0.5em
}
`;
}
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.showImage && this.person && this.person.image) ? this.person.image : null;
return html`
<div class="dropdown" @click="${this.onDropdownClick}">
<a href="#">
<div class="dropdown-trigger login-button">
<div class="name">${this.name}</div>
<vpu-icon class="menu-icon" name="chevron-down" id="menu-chevron-icon"></vpu-icon>
</div>
</a>
<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="login-box login-button">
<div class="icon">${unsafeHTML(loginSVG)}</div>
<div class="label">${i18n.t('login')}</div>
</div>
</a>
`;
}
this._bus.close();
render() {
const loggedIn = (this._loginStatus === LoginStatus.LOGGED_IN);
return html`
<div class="authbox">
${loggedIn ? this.renderLoggedIn() : this.renderLoggedOut()}
</div>
`;
super.disconnectedCallback();
}
}
\ No newline at end of file
import {Auth} from './auth.js';
import {AuthButton} from './auth-button.js';
export {Auth};
\ No newline at end of file
export {Auth, AuthButton};
\ No newline at end of file
export const LoginStatus = Object.freeze({
UNKNOWN: 'unknown',
LOGGING_IN: 'logging-in',
LOGGED_IN: 'logged-in',
LOGGING_OUT: 'logging-out',
LOGGED_OUT: 'logged-out',
});
......@@ -2,6 +2,7 @@ import {i18n} from './i18n.js';
import {html, LitElement} from 'lit-element';
import {ScopedElementsMixin} from '@open-wc/scoped-elements';
import {Auth} from './auth.js';
import {AuthButton} from './auth-button.js';
import * as commonUtils from 'vpu-common/utils';
class AuthDemo extends ScopedElementsMixin(LitElement) {
......@@ -13,6 +14,7 @@ class AuthDemo extends ScopedElementsMixin(LitElement) {
static get scopedElements() {
return {
'vpu-auth': Auth,
'vpu-auth-button': AuthButton,
};
}
......@@ -88,7 +90,9 @@ class AuthDemo extends ScopedElementsMixin(LitElement) {
<h1 class="title">Auth-Demo</h1>
</div>
<div class="container">
<vpu-auth lang="${this.lang}" keycloak-config='{"url": "https://auth-dev.tugraz.at/auth", "realm": "tugraz", "clientId": "auth-dev-mw-frontend-local", "silentCheckSsoRedirectUri": "${silentCheckSsoUri}", "scope": "optional-test-scope"}' load-person try-login show-image></vpu-auth>
<vpu-auth lang="${this.lang}" keycloak-config='{"url": "https://auth-dev.tugraz.at/auth", "realm": "tugraz", "clientId": "auth-dev-mw-frontend-local", "silentCheckSsoRedirectUri": "${silentCheckSsoUri}", "scope": "optional-test-scope"}' load-person try-login></vpu-auth>
<vpu-auth-button lang="${this.lang}" show-image></vpu-auth-button>
</div>
</section>
......
import * as commonUtils from 'vpu-common/utils';
import {defineCustomElement} from 'vpu-common/utils';
import {Auth} from './auth.js';
import {AuthButton} from './auth-button.js';
commonUtils.defineCustomElement('vpu-auth', Auth);
defineCustomElement('vpu-auth', Auth);
defineCustomElement('vpu-auth-button', AuthButton);
......@@ -26,6 +26,24 @@ suite('vpu-auth basics', () => {
});
});
suite('vpu-auth-button', () => {
let node;
suiteSetup(async () => {
node = document.createElement('vpu-auth-button');
document.body.appendChild(node);
await node.updateComplete;
});
suiteTeardown(() => {
node.remove();
});
test('should render', () => {
expect(node).to.have.property('shadowRoot');
});
});
suite('vpu-auth-demo basics', () => {
let node;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment