Skip to content
Commits on Source (65)
......@@ -27,3 +27,21 @@ There is an automatic publishing process initiated for each package if code is p
to the `master` branch, if the package isn't set to private in its `package.json` and
the version number in its `package.json` is higher than the version number on npmjs.com.
## Reserved attributes
| Attribute | Description |
| ------------------------ | ------------------------------------------------------------------- |
| `subscribe` | Used in all components to subscribe to attributes set by a provider |
| `unsubscribe` | Reserved for future use |
| `auth` | Authentication information, set by the authentication component |
| `lang` | Currently selected language, set by the language selector |
| `entry-point-url` | Entry point url for all api requests |
| `requested-login-status` | Used by the login buttons to trigger a login in auth components |
## Reserved events
| Event | Description |
| ------------------ | ---------------------------------------------------------------------------------- |
| `dbp-subscribe` | Event to tell a provider that the component wants to subscribe to an attribute |
| `dbp-unsubscribe` | Event to tell a provider that the component wants to unsubscribe from an attribute |
| `dbp-set-property` | Event to tell a provider that a property should be changed |
......@@ -11,7 +11,10 @@
"test": "lerna run test",
"build": "lerna run build",
"version-patch": "lerna version patch",
"version-minor": "lerna version minor",
"version": "lerna version",
"yarn-install": "for d in ./packages/*/ ; do (cd \"$d\" && yarn install); done;",
"rm-dist": "for d in ./packages/*/ ; do (cd \"$d\" && rm dist -Rf); done;",
"lint": "lerna run lint",
"publish": "lerna publish from-package --yes"
},
......
{
"name": "@dbp-toolkit/app-shell",
"homepage": "https://gitlab.tugraz.at/dbp/web-components/toolkit/-/tree/master/packages/app-shell",
"version": "0.1.7",
"version": "0.2.0",
"main": "src/index.js",
"license": "LGPL-2.1-or-later",
"repository": {
......@@ -34,16 +34,16 @@
"rollup-plugin-serve": "^1.0.1"
},
"dependencies": {
"@dbp-toolkit/auth": "^0.1.6",
"@dbp-toolkit/common": "^0.1.4",
"@dbp-toolkit/language-select": "^0.1.5",
"@dbp-toolkit/matomo": "^0.1.3",
"@dbp-toolkit/notification": "^0.1.4",
"@dbp-toolkit/person-profile": "^0.1.3",
"@dbp-toolkit/auth": "^0.2.0",
"@dbp-toolkit/common": "^0.2.0",
"@dbp-toolkit/language-select": "^0.2.0",
"@dbp-toolkit/matomo": "^0.2.0",
"@dbp-toolkit/notification": "^0.2.0",
"@dbp-toolkit/person-profile": "^0.2.0",
"@open-wc/scoped-elements": "^1.3.2",
"i18next": "^19.8.4",
"lit-element": "^2.3.1",
"lit-html": "^1.1.1",
"lit-element": "^2.4.0",
"lit-html": "^1.3.0",
"universal-router": "^9.0.1"
},
"scripts": {
......
......@@ -2,7 +2,7 @@ import {createI18nInstance} from './i18n.js';
import {html, css} from 'lit-element';
import {ScopedElementsMixin} from '@open-wc/scoped-elements';
import {LanguageSelect} from '@dbp-toolkit/language-select';
import {Icon, EventBus} from '@dbp-toolkit/common';
import {Icon} from '@dbp-toolkit/common';
import {AuthKeycloak} from '@dbp-toolkit/auth';
import {AuthMenuButton} from './auth-menu-button.js';
import {Notification} from '@dbp-toolkit/notification';
......@@ -60,8 +60,6 @@ export class AppShell extends ScopedElementsMixin(AdapterLitElement) {
this.env = '';
this.buildUrl = '';
this.buildTime = '';
this._updateAuth = this._updateAuth.bind(this);
this._loginStatus = 'unknown';
this.matomoUrl = '';
......@@ -73,6 +71,7 @@ export class AppShell extends ScopedElementsMixin(AdapterLitElement) {
this.shellName = 'TU Graz';
this.shellSubname= 'Graz University of Technology';
this.noBrand = false;
this.auth = {};
}
static get scopedElements() {
......@@ -227,7 +226,8 @@ export class AppShell extends ScopedElementsMixin(AdapterLitElement) {
}
static get properties() {
return this.getProperties({
return {
...super.properties,
lang: { type: String, reflect: true },
src: { type: String },
basePath: { type: String, attribute: 'base-path' },
......@@ -249,43 +249,22 @@ export class AppShell extends ScopedElementsMixin(AdapterLitElement) {
buildUrl: { type: String, attribute: "build-url" },
buildTime: { type: String, attribute: "build-time" },
env: { type: String },
});
}
_updateAuth(login) {
if (login.status != this._loginStatus) {
console.log('Login status: ' + login.status);
}
this._loginStatus = login.status;
// Clear the session storage when the user logs out
if (this._loginStatus === 'logging-out') {
sessionStorage.clear();
}
auth: { type: Object },
};
}
connectedCallback() {
super.connectedCallback();
this._bus = new EventBus();
if (this.src)
this.fetchMetadata(this.src);
this.initRouter();
this._bus.subscribe('auth-update', this._updateAuth);
this.updateComplete.then(()=> {
this.matomo = this.shadowRoot.querySelector(this.constructor.getScopedTagName('dbp-matomo'));
});
}
disconnectedCallback() {
this._bus.close();
super.disconnectedCallback();
}
/**
* Switches language if another language is requested
*
......@@ -296,21 +275,39 @@ export class AppShell extends ScopedElementsMixin(AdapterLitElement) {
this.lang = lang;
this.router.update();
const event = new CustomEvent("dbp-language-changed", {
bubbles: true,
detail: {'lang': lang}
});
this.dispatchEvent(event);
// tell a dbp-provider to update the "lang" property
this.sendSetPropertyEvent('lang', lang);
}
}
update(changedProperties) {
changedProperties.forEach((oldValue, propName) => {
if (propName === "lang") {
// For screen readers
document.documentElement.setAttribute("lang", this.lang);
i18n.changeLanguage(this.lang);
switch (propName) {
case 'lang':
i18n.changeLanguage(this.lang);
// For screen readers
document.documentElement.setAttribute("lang", this.lang);
i18n.changeLanguage(this.lang);
this.router.update();
this.subtitle = this.activeMetaDataText("short_name");
this.description = this.activeMetaDataText("description");
break;
case 'auth':
{
const loginStatus = this.auth['login-status'];
if (loginStatus !== this._loginStatus) {
console.log('Login status: ' + loginStatus);
}
this._loginStatus = loginStatus;
// Clear the session storage when the user logs out
if (this._loginStatus === 'logging-out') {
sessionStorage.clear();
}
}
break;
}
});
......@@ -814,19 +811,19 @@ export class AppShell extends ScopedElementsMixin(AdapterLitElement) {
return html`
<slot class="${slotClassMap}"></slot>
<dbp-auth-keycloak lang="${this.lang}" entry-point-url="${this.entryPointUrl}" url="${kc.url}" realm="${kc.realm}" client-id="${kc.clientId}" silent-check-sso-redirect-uri="${kc.silentCheckSsoRedirectUri || ''}" scope="${kc.scope || ''}" idp-hint="${kc.idpHint || ''}" load-person ?force-login="${kc.forceLogin}" ?try-login="${!kc.forceLogin}"></dbp-auth-keycloak>
<dbp-matomo endpoint="${this.matomoUrl}" site-id="${this.matomoSiteId}" git-info="${this.gitInfo}"></dbp-matomo>
<dbp-auth-keycloak subscribe="requested-login-status" lang="${this.lang}" entry-point-url="${this.entryPointUrl}" url="${kc.url}" realm="${kc.realm}" client-id="${kc.clientId}" silent-check-sso-redirect-uri="${kc.silentCheckSsoRedirectUri || ''}" scope="${kc.scope || ''}" idp-hint="${kc.idpHint || ''}" load-person ?force-login="${kc.forceLogin}" ?try-login="${!kc.forceLogin}"></dbp-auth-keycloak>
<dbp-matomo subscribe="auth" endpoint="${this.matomoUrl}" site-id="${this.matomoSiteId}" git-info="${this.gitInfo}"></dbp-matomo>
<div class="${mainClassMap}">
<div id="main">
<dbp-notification lang="${this.lang}"></dbp-notification>
<header>
<div class="hd1-left">
<dbp-language-select @dbp-language-changed=${this.onLanguageChanged.bind(this)}></dbp-language-select>
<dbp-language-select subscribe="lang"></dbp-language-select>
</div>
<div class="hd1-middle">
</div>
<div class="hd1-right">
<dbp-auth-menu-button class="auth-button" lang="${this.lang}"></dbp-auth-menu-button>
<dbp-auth-menu-button subscribe="auth" class="auth-button" lang="${this.lang}"></dbp-auth-menu-button>
</div>
<div class="hd2-left">
<div class="header">
......
......@@ -3,8 +3,9 @@ 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 '@dbp-toolkit/common/styles';
import {Icon, EventBus} from '@dbp-toolkit/common';
import {Icon} from '@dbp-toolkit/common';
import {AdapterLitElement} from "@dbp-toolkit/provider/src/adapter-lit-element";
import {LoginStatus} from "@dbp-toolkit/auth/src/util";
const i18n = createI18nInstance();
......@@ -15,7 +16,7 @@ export class AuthMenuButton extends ScopedElementsMixin(AdapterLitElement) {
super();
this.lang = 'de';
this.showImage = false;
this._loginData = {};
this.auth = {};
this.closeDropdown = this.closeDropdown.bind(this);
this.onWindowResize = this.onWindowResize.bind(this);
......@@ -28,11 +29,12 @@ export class AuthMenuButton extends ScopedElementsMixin(AdapterLitElement) {
}
static get properties() {
return this.getProperties({
return {
...super.properties,
lang: { type: String },
showImage: { type: Boolean, attribute: 'show-image' },
_loginData: { type: Object, attribute: false },
});
auth: { type: Object },
};
}
onWindowResize() {
......@@ -42,18 +44,12 @@ export class AuthMenuButton extends ScopedElementsMixin(AdapterLitElement) {
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();
}
......@@ -75,12 +71,12 @@ export class AuthMenuButton extends ScopedElementsMixin(AdapterLitElement) {
}
onLoginClicked(e) {
this._bus.publish('auth-login');
this.sendSetPropertyEvent('requested-login-status', LoginStatus.LOGGED_IN);
e.preventDefault();
}
onLogoutClicked(e) {
this._bus.publish('auth-logout');
this.sendSetPropertyEvent('requested-login-status', LoginStatus.LOGGED_OUT);
}
update(changedProperties) {
......@@ -250,14 +246,14 @@ export class AuthMenuButton extends ScopedElementsMixin(AdapterLitElement) {
}
renderLoggedIn() {
const person = this._loginData.person;
const person = this.auth.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>
<div class="name">${this.auth['user-full-name']}</div>
<dbp-icon class="menu-icon" name="chevron-down" id="menu-chevron-icon"></dbp-icon>
</div>
</a>
......@@ -306,7 +302,7 @@ export class AuthMenuButton extends ScopedElementsMixin(AdapterLitElement) {
}
render() {
const loggedIn = (this._loginData.status === 'logged-in');
const loggedIn = (this.auth['login-status'] === 'logged-in');
return html`
<div class="authbox">
${loggedIn ? this.renderLoggedIn() : this.renderLoggedOut()}
......
......@@ -13,12 +13,13 @@ export class BuildInfo extends AdapterLitElement {
}
static get properties() {
return this.getProperties({
return {
...super.properties,
env: { type: String },
buildUrl: { type: String, attribute: "build-url" },
buildTime: { type: String, attribute: "build-time" },
gitInfo: { type: String, attribute: "git-info" }
});
};
}
static get styles() {
......
......@@ -14,9 +14,10 @@ export class TUGrazLogo extends AdapterLitElement {
}
static get properties() {
return this.getProperties({
return {
...super.properties,
lang: { type: String }
});
};
}
update(changedProperties) {
......
......@@ -20,13 +20,16 @@ npm i @dbp-toolkit/auth
- `lang` (optional, default: `de`): set to `de` or `en` for German or English
- example `<dbp-auth-keycloak lang="de" </dbp-auth-keycloak>`
- `load-person` (optional, default: off): if enabled the logged in user will also be loaded as `Person`
in the `window.DBPPerson` variable
in the `auth.person` attribute (see below)
- example `<dbp-auth-keycloak load-person></dbp-auth-keycloak>`
- `force-login` (optional, default: off): if enabled a login will be forced, there never will be a login button
- example `<dbp-auth-keycloak force-login></dbp-auth-keycloak>`
- `try-login` (optional, default: off): if enabled the a login will happen if the user is already logged in
and finishing the login process would not result in a page location change (reload/redirect).
- example `<dbp-auth-keycloak try-login></dbp-auth-keycloak>`
- `requested-login-status` (optional, default: `unknown`): can be set to `logged-in` or `logged-out` to request a login or logout
- example `<dbp-auth-keycloak requested-login-status="logged-in"></dbp-auth-keycloak>`
- note: most often this should be an attribute that is not set directly, but subscribed at a provider
### Keycloak Specific Attributes
......@@ -36,11 +39,17 @@ npm i @dbp-toolkit/auth
- `silent-check-sso-redirect-uri` (optional): URI or path to a separate page for checking the login session in an iframe, see https://www.keycloak.org/docs/latest/securing_apps/#_javascript_adapter
- `scope` (optional): Space separated list of scopes to request. These scopes get added in addition to the default ones, assuming the scope is in the optional scopes list of the Keycloak client in use.
### Events to listen to
### Emitted attribute
The component emits a `dbp-set-property` event for the attribute `auth`:
- `auth.subject`: Keycloak username
- `auth.login-status`: Login status (`unknown`, `logging-in`, `logging-out`, `logged-in`, `logged-out`)
- `auth.token`: Keycloak token to send with your requests
- `auth.user-full-name`: Full name of the user
- `auth.person-id`: Person identifier of the user
- `auth.person`: Person json object of the user (optional, enable by setting the `load-person` attribute)
- `dbp-auth-init`: Keycloak init event - happens once
- `dbp-auth-person-init`: Keycloak person init event - the person entity was loaded from the server
- `dbp-auth-keycloak-data-update`: Keycloak data was updated - happens for example every time after a token refresh
## Login Button
......@@ -51,6 +60,14 @@ npm i @dbp-toolkit/auth
<script type="module" src="node_modules/@dbp-toolkit/auth/dist/dbp-auth.js"></script>
```
### Attributes
- `lang` (optional, default: `de`): set to `de` or `en` for German or English
- example `<dbp-auth-keycloak lang="de" </dbp-auth-keycloak>`
- `auth` object: you need to set that object property for the auth token
- example auth property: `{token: "THE_BEARER_TOKEN"}`
- note: most often this should be an attribute that is not set directly, but subscribed at a provider
## Local development
```bash
......
......@@ -3,11 +3,24 @@
<head>
<meta charset="UTF-8">
<script type="module" src="dbp-auth-demo.js"></script>
<script type="module" src="dbp-auth.js"></script>
</head>
<body>
<dbp-auth-demo lang="de" entry-point-url="http://127.0.0.1:8000"></dbp-auth-demo>
<dbp-provider auth requested-login-status>
<dbp-auth-keycloak subscribe="requested-login-status" lang="de" entry-point-url="http://127.0.0.1:8000"
url="https://auth-dev.tugraz.at/auth"
realm="tugraz"
client-id="auth-dev-mw-frontend-local"
silent-check-sso-redirect-uri="${silentCheckSsoUri}"
scope="optional-test-scope"
load-person
try-login></dbp-auth-keycloak>
<dbp-login-button subscribe="auth" lang="de" show-image></dbp-login-button>
<dbp-auth-demo lang="de" entry-point-url="http://127.0.0.1:8000" subscribe="auth"></dbp-auth-demo>
</dbp-provider>
</body>
</html>
{
"name": "@dbp-toolkit/auth",
"homepage": "https://gitlab.tugraz.at/dbp/web-components/toolkit/-/tree/master/packages/auth",
"version": "0.1.6",
"version": "0.2.0",
"main": "src/index.js",
"license": "LGPL-2.1-or-later",
"repository": {
......@@ -37,10 +37,10 @@
"rollup-plugin-terser": "^7.0.2"
},
"dependencies": {
"@dbp-toolkit/common": "^0.1.4",
"@dbp-toolkit/common": "^0.2.0",
"@open-wc/scoped-elements": "^1.3.2",
"event-target-shim": "^6.0.0",
"lit-element": "^2.3.1"
"lit-element": "^2.4.0"
},
"scripts": {
"clean": "rm dist/*",
......
import {i18n} from './i18n.js';
import JSONLD from '@dbp-toolkit/common/jsonld';
import {EventBus} from '@dbp-toolkit/common';
import {KeycloakWrapper} from './keycloak.js';
import {LoginStatus} from './util.js';
import {AdapterLitElement} from "@dbp-toolkit/provider/src/adapter-lit-element";
......@@ -10,14 +9,13 @@ import {AdapterLitElement} from "@dbp-toolkit/provider/src/adapter-lit-element";
* Keycloak auth web component
* https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter
*
* Dispatches an event `dbp-auth-init` and sets some global variables:
* window.DBPAuthSubject: Keycloak username
* window.DBPAuthToken: Keycloak token to send with your requests
* window.DBPAuthTokenParsed: Keycloak token content
* window.DBPUserFullName: Full name of the user
* window.DBPPersonId: Person identifier of the user
* window.DBPPerson: Person json object of the user (optional, enable by setting the `load-person` attribute,
* which will dispatch a `dbp-auth-person-init` event when loaded)
* Emits a dbp-set-property event for the attribute "auth":
* auth.subject: Keycloak username
* auth.login-status: Login status (see object LoginStatus)
* auth.token: Keycloak token to send with your requests
* auth.user-full-name: Full name of the user
* auth.person-id: Person identifier of the user
* auth.person: Person json object of the user (optional, enable by setting the `load-person` attribute)
*/
export class AuthKeycloak extends AdapterLitElement {
constructor() {
......@@ -26,7 +24,6 @@ export class AuthKeycloak extends AdapterLitElement {
this.forceLogin = false;
this.loadPerson = false;
this.token = "";
this.tokenParsed = null;
this.subject = "";
this.name = "";
this.personId = "";
......@@ -34,6 +31,7 @@ export class AuthKeycloak extends AdapterLitElement {
this.person = null;
this.entryPointUrl = '';
this._loginStatus = LoginStatus.UNKNOWN;
this.requestedLoginStatus = LoginStatus.UNKNOWN;
// Keycloak config
this.keycloakUrl = null;
......@@ -43,18 +41,37 @@ export class AuthKeycloak extends AdapterLitElement {
this.scope = null;
this.idpHint = '';
// Create the events
this.initEvent = new CustomEvent("dbp-auth-init", { "detail": "KeyCloak init event", bubbles: true, composed: true });
this.personInitEvent = new CustomEvent("dbp-auth-person-init", { "detail": "KeyCloak person init event", bubbles: true, composed: true });
this.keycloakDataUpdateEvent = new CustomEvent("dbp-auth-keycloak-data-update", { "detail": "KeyCloak data was updated", bubbles: true, composed: true });
this._onKCChanged = this._onKCChanged.bind(this);
}
update(changedProperties) {
// console.log("changedProperties", changedProperties);
changedProperties.forEach((oldValue, propName) => {
if (propName === "lang") {
i18n.changeLanguage(this.lang);
switch (propName) {
case 'lang':
i18n.changeLanguage(this.lang);
break;
case 'requestedLoginStatus':
console.log("requested-login-status changed", this.requestedLoginStatus);
switch(this.requestedLoginStatus) {
case LoginStatus.LOGGED_IN:
this._kcwrapper.login({lang: this.lang, scope: this.scope || ''});
break;
case LoginStatus.LOGGED_OUT:
// 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);
}
break;
}
break;
}
});
......@@ -67,7 +84,6 @@ export class AuthKeycloak extends AdapterLitElement {
if (kc.authenticated) {
let tokenChanged = (this.token !== kc.token);
this.tokenParsed = kc.tokenParsed;
this.name = kc.idTokenParsed.name;
this.token = kc.token;
......@@ -79,13 +95,6 @@ export class AuthKeycloak extends AdapterLitElement {
}
this.personId = personId;
window.DBPAuthSubject = this.subject;
window.DBPAuthToken = this.token;
window.DBPAuthTokenParsed = this.tokenParsed;
window.DBPUserFullName = this.name;
window.DBPPersonId = this.personId;
window.DBPPerson = this.person;
this.sendSetPropertyEvents();
this._setLoginStatus(LoginStatus.LOGGED_IN, tokenChanged || newPerson);
} else {
......@@ -94,28 +103,16 @@ export class AuthKeycloak extends AdapterLitElement {
}
this.name = "";
this.token = "";
this.tokenParsed = null;
this.subject = "";
this.personId = "";
this.person = null;
window.DBPAuthSubject = this.subject;
window.DBPAuthToken = this.token;
window.DBPAuthTokenParsed = this.tokenParsed;
window.DBPUserFullName = this.name;
window.DBPPersonId = this.personId;
window.DBPPerson = this.person;
this.sendSetPropertyEvents();
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
......@@ -132,23 +129,36 @@ export class AuthKeycloak extends AdapterLitElement {
.then(response => response.json())
.then((person) => {
that.person = person;
window.DBPPerson = person;
that.dispatchEvent(that.personInitEvent);
this.sendSetPropertyEvents();
this._setLoginStatus(this._loginStatus, true);
});
}, {}, that.lang);
}
this.dispatchEvent(this.keycloakDataUpdateEvent);
}
sendSetPropertyEvents() {
this.sendSetPropertyEvent('auth-subject', this.subject);
this.sendSetPropertyEvent('auth-token', this.token);
this.sendSetPropertyEvent('auth-token-parsed', this.tokenParsed);
this.sendSetPropertyEvent('user-full-name', this.name);
this.sendSetPropertyEvent('person-id', this.personId);
this.sendSetPropertyEvent('person', this.person);
const auth = {
'login-status': this._loginStatus,
'subject': this.subject,
'token': this.token,
'user-full-name': this.name,
'person-id': this.personId,
'person': this.person,
};
// inject a window.DBPAuth variable for cypress
if (window.Cypress) {
window.DBPAuth = auth;
}
this.sendSetPropertyEvent('auth', auth);
JSONLD.doInitializationOnce(this.entryPointUrl, this.token);
// this.sendSetPropertyEvent('auth-subject', this.subject);
// this.sendSetPropertyEvent('auth-token', this.token);
// this.sendSetPropertyEvent('user-full-name', this.name);
// this.sendSetPropertyEvent('person-id', this.personId);
// this.sendSetPropertyEvent('person', this.person);
}
_setLoginStatus(status, force) {
......@@ -156,19 +166,12 @@ export class AuthKeycloak extends AdapterLitElement {
return;
this._loginStatus = status;
this._bus.publish('auth-update', {
status: this._loginStatus,
token: this.token,
name: this.name,
person: this.person,
}, {
retain: true,
});
this.sendSetPropertyEvents();
}
static get properties() {
return this.getProperties({
return {
...super.properties,
lang: { type: String },
forceLogin: { type: Boolean, attribute: 'force-login' },
tryLogin: { type: Boolean, attribute: 'try-login' },
......@@ -186,7 +189,8 @@ export class AuthKeycloak extends AdapterLitElement {
silentCheckSsoRedirectUri: { type: String, attribute: 'silent-check-sso-redirect-uri' },
scope: { type: String },
idpHint: { type: String, attribute: 'idp-hint' },
});
requestedLoginStatus: { type: String, attribute: 'requested-login-status' },
};
}
connectedCallback() {
......@@ -199,28 +203,9 @@ export class AuthKeycloak extends AdapterLitElement {
if (!this.clientId)
throw Error("client-id not set");
this._bus = new EventBus();
this._kcwrapper = new KeycloakWrapper(this.keycloakUrl, this.realm, this.clientId, this.silentCheckSsoRedirectUri, this.idpHint);
this._kcwrapper.addEventListener('changed', this._onKCChanged);
this._bus.subscribe('auth-login', () => {
this._kcwrapper.login({lang: this.lang, scope: this.scope || ''});
});
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);
......@@ -241,7 +226,6 @@ export class AuthKeycloak extends AdapterLitElement {
disconnectedCallback() {
this._kcwrapper.close();
this._kcwrapper.removeEventListener('changed', this._onKCChanged);
this._bus.close();
super.disconnectedCallback();
}
......
......@@ -6,12 +6,14 @@ import {LoginButton} from './login-button.js';
import * as commonUtils from '@dbp-toolkit/common/utils';
import {name as pkgName} from './../package.json';
import DBPLitElement from "@dbp-toolkit/common/dbp-lit-element";
import {Provider} from '@dbp-toolkit/provider';
class AuthDemo extends ScopedElementsMixin(DBPLitElement) {
export class DbpAuthDemo extends ScopedElementsMixin(DBPLitElement) {
constructor() {
super();
this.lang = 'de';
this.entryPointUrl = '';
this.auth = {};
}
static get scopedElements() {
......@@ -22,10 +24,12 @@ class AuthDemo extends ScopedElementsMixin(DBPLitElement) {
}
static get properties() {
return this.getProperties({
return {
...super.properties,
lang: { type: String },
entryPointUrl: { type: String, attribute: 'entry-point-url' },
});
auth: { type: Object },
};
}
update(changedProperties) {
......@@ -39,8 +43,10 @@ class AuthDemo extends ScopedElementsMixin(DBPLitElement) {
}
async _onUserInfoClick() {
if (!window.DBPAuthToken) {
const div = this._('#person-info');
if (!this.auth.token) {
console.error("not logged in");
div.innerHTML = "You are not logged in!";
return;
}
let userInfoURL = 'https://auth-dev.tugraz.at/auth/realms/tugraz/protocol/openid-connect/userinfo';
......@@ -50,20 +56,25 @@ class AuthDemo extends ScopedElementsMixin(DBPLitElement) {
userInfoURL, {
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + window.DBPAuthToken
'Authorization': 'Bearer ' + this.auth.token
}
}
);
console.log(await response.json());
const person = await response.json();
console.log(person);
div.innerHTML = JSON.stringify(person);
}
async _onShowToken() {
if (!window.DBPAuthToken) {
const div = this._('#token-info');
if (!this.auth.token) {
console.error("not logged in");
div.innerHTML = "You are not logged in!";
return;
}
console.log(window.DBPAuthTokenParsed);
console.log(this.auth.token);
div.innerHTML = this.auth.token;
}
render() {
......@@ -89,21 +100,39 @@ class AuthDemo extends ScopedElementsMixin(DBPLitElement) {
max-width: 100%;
}
</style>
<slot></slot>
<section class="section">
<div class="container">
<h1 class="title">Auth-Demo</h1>
</div>
<div class="container">
<dbp-auth-keycloak lang="${this.lang}" entry-point-url="${this.entryPointUrl}" url="https://auth-dev.tugraz.at/auth" realm="tugraz" client-id="auth-dev-mw-frontend-local" silent-check-sso-redirect-uri="${silentCheckSsoUri}" scope="optional-test-scope" load-person try-login></dbp-auth-keycloak>
<dbp-login-button lang="${this.lang}" show-image></dbp-login-button>
<p>In any App/page an <b>Auth component</b> will be used like:</p>
<pre>
&lt;dbp-auth-keycloak lang="${this.lang}" entry-point-url="${this.entryPointUrl}"
url="https://auth-dev.tugraz.at/auth"
realm="tugraz"
client-id="auth-dev-mw-frontend-local"
silent-check-sso-redirect-uri="${silentCheckSsoUri}"
scope="optional-test-scope"
load-person
try-login&gt;&lt;/dbp-auth-keycloak&gt;
&lt;dbp-login-button lang="${this.lang}" show-image&gt;&lt;/dbp-login-button&gt;
</pre>
<p>but in this demo we actually have already such a component at the top of this page.</p>
<p>Please use the login button on the top of the page!</p>
</div>
<div class="container">
<input type="button" value="Fetch userinfo" @click="${this._onUserInfoClick}">
<input type="button" value="Show token" @click="${this._onShowToken}">
<h4>Person info:</h4>
<div id="person-info"></div>
<h4>Token info:</h4>
<div id="token-info"></div>
</div>
</section>
<input type="button" value="Fetch userinfo (see console)" @click="${this._onUserInfoClick}">
<input type="button" value="Show token (see console)" @click="${this._onShowToken}">
`;
}
}
commonUtils.defineCustomElement('dbp-auth-demo', AuthDemo);
commonUtils.defineCustomElement('dbp-provider', Provider);
commonUtils.defineCustomElement('dbp-auth-demo', DbpAuthDemo);
......@@ -3,9 +3,8 @@ 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 '@dbp-toolkit/common/styles';
import {LitElement} from "lit-element";
import {EventBus} from '@dbp-toolkit/common';
import {LoginStatus} from './util.js';
import {AdapterLitElement} from "../../provider/src/adapter-lit-element";
let logoutSVG = `
<svg
......@@ -63,12 +62,12 @@ let loggingInSVG = `
</svg>
`;
export class LoginButton extends ScopedElementsMixin(LitElement) {
export class LoginButton extends ScopedElementsMixin(AdapterLitElement) {
constructor() {
super();
this.lang = 'de';
this._loginData = {};
this.auth = {};
}
static get scopedElements() {
......@@ -79,32 +78,25 @@ export class LoginButton extends ScopedElementsMixin(LitElement) {
static get properties() {
return {
lang: { type: String },
_loginData: { type: Object, attribute: false },
auth: { type: Object },
};
}
connectedCallback() {
super.connectedCallback();
this._bus = new EventBus();
this._bus.subscribe('auth-update', (data) => {
console.log(data);
this._loginData = data;
});
}
disconnectedCallback() {
this._bus.close();
super.disconnectedCallback();
}
_onLoginClicked(e) {
this._bus.publish('auth-login');
this.sendSetPropertyEvent('requested-login-status', LoginStatus.LOGGED_IN);
e.preventDefault();
}
_onLogoutClicked(e) {
this._bus.publish('auth-logout');
this.sendSetPropertyEvent('requested-login-status', LoginStatus.LOGGED_OUT);
e.preventDefault();
}
......@@ -165,7 +157,7 @@ export class LoginButton extends ScopedElementsMixin(LitElement) {
}
render() {
if (this._loginData.status === LoginStatus.LOGGING_IN) {
if (this.auth['login-status'] === LoginStatus.LOGGING_IN) {
// try to keep the layout the same to avoid layout shifts
return html`
<a href="#">
......@@ -175,7 +167,7 @@ export class LoginButton extends ScopedElementsMixin(LitElement) {
</div>
</a>
`;
} else if (this._loginData.status === LoginStatus.LOGGED_IN) {
} else if (this.auth['login-status'] === LoginStatus.LOGGED_IN) {
return html`
<a href="#" @click="${this._onLogoutClicked}">
<div class="login-box login-button">
......
......@@ -32,6 +32,9 @@ npm i @dbp-toolkit/check-in-place-select
- example `<dbp-check-in-place-select show-reload-button></dbp-check-in-place-select>`
- `reload-button-title` (optional): sets a title text on the reload button
- example `<dbp-check-in-place-select show-reload-button reload-button-text="Reload result list"></dbp-check-in-place-select>`
- `auth` object: you need to set that object property for the auth token
- example auth property: `{token: "THE_BEARER_TOKEN"}`
- note: most often this should be an attribute that is not set directly, but subscribed at a provider
## Local development
......
......@@ -8,6 +8,6 @@
<body>
<dbp-check-in-place-select-demo lang="de" entry-point-url="http://127.0.0.1:8000"></dbp-check-in-place-select-demo>
<dbp-check-in-place-select-demo lang="de" entry-point-url="http://127.0.0.1:8000" auth requested-login-status></dbp-check-in-place-select-demo>
</body>
</html>
{
"name": "@dbp-toolkit/check-in-place-select",
"homepage": "https://gitlab.tugraz.at/dbp/web-components/toolkit/-/tree/master/packages/check-in-place-select",
"version": "0.1.3",
"version": "0.2.0",
"main": "src/index.js",
"license": "LGPL-2.1-or-later",
"repository": {
......@@ -35,11 +35,11 @@
"rollup-plugin-terser": "^7.0.2"
},
"dependencies": {
"@dbp-toolkit/auth": "^0.1.6",
"@dbp-toolkit/common": "^0.1.4",
"@dbp-toolkit/auth": "^0.2.0",
"@dbp-toolkit/common": "^0.2.0",
"@open-wc/scoped-elements": "^1.3.2",
"jquery": "^3.4.1",
"lit-element": "^2.3.1",
"lit-element": "^2.4.0",
"select2": "^4.0.10"
},
"scripts": {
......
......@@ -39,6 +39,7 @@ export class CheckInPlaceSelect extends ScopedElementsMixin(AdapterLitElement) {
this.showReloadButton = false;
this.reloadButtonTitle = '';
this.showCapacity = false;
this.auth = {};
this._onDocumentClicked = this._onDocumentClicked.bind(this);
}
......@@ -57,7 +58,8 @@ export class CheckInPlaceSelect extends ScopedElementsMixin(AdapterLitElement) {
}
static get properties() {
return this.getProperties({
return {
...super.properties,
lang: { type: String },
active: { type: Boolean, attribute: false },
entryPointUrl: { type: String, attribute: 'entry-point-url' },
......@@ -66,7 +68,8 @@ export class CheckInPlaceSelect extends ScopedElementsMixin(AdapterLitElement) {
showReloadButton: { type: Boolean, attribute: 'show-reload-button' },
reloadButtonTitle: { type: String, attribute: 'reload-button-title' },
showCapacity: { type: Boolean, attribute: 'show-capacity' },
});
auth: { type: Object },
};
}
clear() {
......@@ -162,7 +165,7 @@ export class CheckInPlaceSelect extends ScopedElementsMixin(AdapterLitElement) {
url: apiUrl,
contentType: "application/ld+json",
beforeSend: function (jqXHR) {
jqXHR.setRequestHeader('Authorization', 'Bearer ' + window.DBPAuthToken);
jqXHR.setRequestHeader('Authorization', 'Bearer ' + that.auth.token);
that.isSearching = true;
},
data: function (params) {
......@@ -234,7 +237,7 @@ export class CheckInPlaceSelect extends ScopedElementsMixin(AdapterLitElement) {
fetch(apiUrl, {
headers: {
'Content-Type': 'application/ld+json',
'Authorization': 'Bearer ' + window.DBPAuthToken,
'Authorization': 'Bearer ' + this.auth.token,
},
})
.then(result => {
......@@ -305,6 +308,9 @@ export class CheckInPlaceSelect extends ScopedElementsMixin(AdapterLitElement) {
// we don't need to preset the selector if the entry point url changes
this.initJSONLD(true);
break;
case "auth":
JSONLD.doInitializationOnce(this.entryPointUrl, this.auth.token);
break;
}
});
......
......@@ -24,11 +24,12 @@ export class CheckInPlaceSelectDemo extends ScopedElementsMixin(DBPLitElement) {
}
static get properties() {
return this.getProperties({
return {
...super.properties,
lang: { type: String },
entryPointUrl: { type: String, attribute: 'entry-point-url' },
noAuth: { type: Boolean, attribute: 'no-auth' },
});
};
}
connectedCallback() {
......@@ -52,12 +53,12 @@ export class CheckInPlaceSelectDemo extends ScopedElementsMixin(DBPLitElement) {
}
getAuthComponentHtml() {
return this.noAuth ? html`<dbp-login-button lang="${this.lang}" show-image></dbp-login-button>` : html`
return this.noAuth ? html`<dbp-login-button subscribe="auth" lang="${this.lang}" show-image></dbp-login-button>` : html`
<div class="container">
<dbp-auth-keycloak lang="${this.lang}" entry-point-url="${this.entryPointUrl}" silent-check-sso-redirect-uri="/dist/silent-check-sso.html"
<dbp-auth-keycloak subscribe="requested-login-status" lang="${this.lang}" entry-point-url="${this.entryPointUrl}" silent-check-sso-redirect-uri="/dist/silent-check-sso.html"
url="https://auth-dev.tugraz.at/auth" realm="tugraz"
client-id="auth-dev-mw-frontend-local" load-person try-login></dbp-auth-keycloak>
<dbp-login-button lang="${this.lang}" show-image></dbp-login-button>
<dbp-login-button subscribe="auth" lang="${this.lang}" show-image></dbp-login-button>
</div>
`;
}
......@@ -74,13 +75,13 @@ export class CheckInPlaceSelectDemo extends ScopedElementsMixin(DBPLitElement) {
<div class="field">
<label class="label">Check-In-Place 1</label>
<div class="control">
<dbp-check-in-place-select lang="${this.lang}" entry-point-url="${this.entryPointUrl}"></dbp-check-in-place-select>
<dbp-check-in-place-select lang="${this.lang}" entry-point-url="${this.entryPointUrl}" subscribe="auth"></dbp-check-in-place-select>
</div>
</div>
<div class="field">
<label class="label">Check-In-Place 2</label>
<div class="control">
<dbp-check-in-place-select lang="${this.lang}" entry-point-url="${this.entryPointUrl}" show-reload-button reload-button-title="Click me"></dbp-check-in-place-select>
<dbp-check-in-place-select lang="${this.lang}" entry-point-url="${this.entryPointUrl}" subscribe="auth" show-reload-button reload-button-title="Click me"></dbp-check-in-place-select>
</div>
</div>
</form>
......
......@@ -43,26 +43,31 @@ export default class JSONLD {
// add success and failure functions
if (typeof successFnc == 'function') successFunctions[apiUrl].push(successFnc);
if (typeof failureFnc == 'function') failureFunctions[apiUrl].push(failureFnc);
}
/**
* This should be run as soon as an api token is available (can be run multiple times)
*
* @param apiUrl
* @param token
*/
static doInitializationOnce(apiUrl, token) {
// console.log("doInitializationOnce", apiUrl, token);
// check if api call was already started
if (initStarted[apiUrl] !== undefined) {
// check if token is not set or api call was already started
if (!apiUrl || !token || initStarted[apiUrl] !== undefined) {
return;
}
initStarted[apiUrl] = true;
if (window.DBPAuthToken !== undefined) {
JSONLD.doInitialization(apiUrl);
} else {
// window.DBPAuthToken will be set by dbp-auth-init event
window.addEventListener("dbp-auth-init", () => JSONLD.doInitialization(apiUrl));
}
JSONLD.doInitialization(apiUrl, token);
// console.log("doInitializationOnce Done", apiUrl, token);
}
static doInitialization(apiUrl) {
static doInitialization(apiUrl, token) {
const xhr = new XMLHttpRequest();
xhr.open("GET", apiUrl, true);
xhr.setRequestHeader('Authorization', 'Bearer ' + window.DBPAuthToken);
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) {
......
{
"name": "@dbp-toolkit/common",
"homepage": "https://gitlab.tugraz.at/dbp/web-components/toolkit/-/tree/master/packages/common",
"version": "0.1.4",
"version": "0.2.0",
"module": "index.js",
"license": "LGPL-2.1-or-later",
"repository": {
......@@ -39,6 +39,6 @@
"@open-wc/scoped-elements": "^1.3.2",
"@sentry/browser": "^5.27.4",
"i18next": "^19.8.4",
"lit-element": "^2.3.1"
"lit-element": "^2.4.0"
}
}