saasufy-components 10.2.0 → 10.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/if-group.js +28 -8
- package/log-out.js +44 -2
- package/package.json +1 -1
- package/switch-group.js +28 -8
package/README.md
CHANGED
|
@@ -936,6 +936,8 @@ The change in the location hash can then be detected by an `app-router` to switc
|
|
|
936
936
|
|
|
937
937
|
A component which can be placed inside a `socket-provider` to deauthenticate the socket (e.g. on click).
|
|
938
938
|
|
|
939
|
+
If the user logged in via an OAuth provider which supports RP-initiated log out (such as Keycloak), deauthenticating the socket does not end the session on the provider itself; the user would be logged straight back in without being asked for their credentials the next time they log in. To also end the provider session, specify the `logout-url` attribute and the component will redirect the user to that endpoint after deauthenticating the socket.
|
|
940
|
+
|
|
939
941
|
**Import**
|
|
940
942
|
|
|
941
943
|
```html
|
|
@@ -948,6 +950,27 @@ A component which can be placed inside a `socket-provider` to deauthenticate the
|
|
|
948
950
|
<log-out onclick="logOut()"><a href="javascript:void(0)">Log out</a></log-out>
|
|
949
951
|
```
|
|
950
952
|
|
|
953
|
+
Logging out of Keycloak as well as of the app:
|
|
954
|
+
|
|
955
|
+
```html
|
|
956
|
+
<log-out
|
|
957
|
+
onclick="logOut()"
|
|
958
|
+
provider="keycloak"
|
|
959
|
+
logout-url="https://auth.example.com/realms/myrealm/protocol/openid-connect/logout"
|
|
960
|
+
client-id="myclient"
|
|
961
|
+
post-logout-redirect-uri="https://myapp.com/index.html"
|
|
962
|
+
><a href="javascript:void(0)">Log out</a></log-out>
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
**Attributes**
|
|
966
|
+
|
|
967
|
+
- `logout-url`: The log out endpoint of the OAuth provider; for OpenID Connect providers this is the `end_session_endpoint`. If it is not specified, the component only deauthenticates the socket. The redirect is skipped if the user did not log in via an OAuth provider.
|
|
968
|
+
- `provider`: The name of the OAuth provider, as specified on the `Authentication` page of your Saasufy control panel. If it is set, the redirect only happens when the user logged in via that specific provider; this is useful when your app offers several different ways to log in.
|
|
969
|
+
- `client-id`: The client ID to pass to the provider as the `client_id` query parameter. OpenID Connect providers require either this or an ID token in order to log the user out without asking them to confirm.
|
|
970
|
+
- `post-logout-redirect-uri`: The URL to send the user back to once the provider has logged them out, passed as the `post_logout_redirect_uri` query parameter. It usually needs to be registered with the provider in advance. If it is not specified, the user is left on the provider's own logged out page.
|
|
971
|
+
|
|
972
|
+
If the auth token contains an ID token (which requires the `idTokenField` of the provider to be set on the `Authentication` page of your Saasufy control panel), it is passed to the provider as the `id_token_hint` query parameter. Providers such as Keycloak show a log out confirmation page when it is missing. Any of these query parameters which are already present in `logout-url` are left untouched.
|
|
973
|
+
|
|
951
974
|
### oauth-link
|
|
952
975
|
|
|
953
976
|
A link to initiate an OAuth flow to authenticate a user as part of log in. Can support a range of different OAuth providers.
|
package/if-group.js
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
import { debouncer } from './utils.js';
|
|
2
|
-
|
|
3
|
-
const DEBOUNCE_DELAY = 0;
|
|
4
|
-
|
|
5
1
|
class IfGroup extends HTMLElement {
|
|
6
2
|
constructor() {
|
|
7
3
|
super();
|
|
8
4
|
this.isReady = false;
|
|
5
|
+
this.renderPending = false;
|
|
6
|
+
this.lastViewportNode = null;
|
|
7
|
+
this.lastRenderedContent = null;
|
|
9
8
|
this.attachShadow({ mode: 'open' });
|
|
10
9
|
this.shadowRoot.innerHTML = '<slot name="content"></slot><slot name="viewport"></slot>';
|
|
11
10
|
|
|
12
|
-
let debounce = debouncer();
|
|
13
|
-
|
|
14
11
|
this.processTemplates = () => {
|
|
15
12
|
let viewportSlot = this.shadowRoot.querySelector('slot[name="viewport"]');
|
|
16
13
|
let viewportNode = viewportSlot.assignedNodes()[0];
|
|
@@ -32,10 +29,33 @@ class IfGroup extends HTMLElement {
|
|
|
32
29
|
for (let node of contentNodes) {
|
|
33
30
|
contentList.push(node.innerHTML);
|
|
34
31
|
}
|
|
35
|
-
|
|
32
|
+
let content = contentList.join('');
|
|
33
|
+
|
|
34
|
+
// Rewriting innerHTML with identical markup recreates the nodes, which loses
|
|
35
|
+
// decoded images, focus and scroll position; greedy-refresh forces that reset.
|
|
36
|
+
if (
|
|
37
|
+
!this.hasAttribute('greedy-refresh') &&
|
|
38
|
+
viewportNode === this.lastViewportNode &&
|
|
39
|
+
content === this.lastRenderedContent
|
|
40
|
+
) return;
|
|
41
|
+
|
|
42
|
+
this.lastViewportNode = viewportNode;
|
|
43
|
+
this.lastRenderedContent = content;
|
|
44
|
+
|
|
45
|
+
viewportNode.innerHTML = content;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Coalescing on a microtask keeps the write ahead of the next paint; deferring
|
|
49
|
+
// with a timeout lets the browser paint the empty viewport first, which flickers.
|
|
50
|
+
this.debouncedProcessTemplates = () => {
|
|
51
|
+
if (this.renderPending) return;
|
|
52
|
+
this.renderPending = true;
|
|
53
|
+
queueMicrotask(() => {
|
|
54
|
+
this.renderPending = false;
|
|
55
|
+
this.processTemplates();
|
|
56
|
+
});
|
|
36
57
|
};
|
|
37
58
|
|
|
38
|
-
this.debouncedProcessTemplates = debounce(this.processTemplates, DEBOUNCE_DELAY);
|
|
39
59
|
this.shadowRoot.addEventListener('slotchange', this.debouncedProcessTemplates);
|
|
40
60
|
}
|
|
41
61
|
|
package/log-out.js
CHANGED
|
@@ -1,8 +1,50 @@
|
|
|
1
1
|
import { SocketConsumer } from './socket.js';
|
|
2
2
|
|
|
3
3
|
class LogOut extends SocketConsumer {
|
|
4
|
-
|
|
5
|
-
this.
|
|
4
|
+
getProviderLogOutURL(authToken) {
|
|
5
|
+
let logoutURL = this.getAttribute('logout-url');
|
|
6
|
+
if (!logoutURL) return null;
|
|
7
|
+
|
|
8
|
+
// RP-initiated log out only applies to sessions which were created via an OAuth provider.
|
|
9
|
+
let authSource = authToken && authToken.authSource;
|
|
10
|
+
if (!authSource) return null;
|
|
11
|
+
let provider = this.getAttribute('provider');
|
|
12
|
+
if (provider && provider !== authSource) return null;
|
|
13
|
+
|
|
14
|
+
let providerLogOutURL;
|
|
15
|
+
try {
|
|
16
|
+
providerLogOutURL = new URL(logoutURL, location.origin);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
'The logout-url attribute of log-out was not a valid URL'
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let clientId = this.getAttribute('client-id');
|
|
24
|
+
let postLogoutRedirectURI = this.getAttribute('post-logout-redirect-uri');
|
|
25
|
+
let searchParams = providerLogOutURL.searchParams;
|
|
26
|
+
|
|
27
|
+
// Without an id_token_hint, the provider asks the user to confirm the log out.
|
|
28
|
+
if (authToken.idToken && !searchParams.has('id_token_hint')) {
|
|
29
|
+
searchParams.set('id_token_hint', authToken.idToken);
|
|
30
|
+
}
|
|
31
|
+
if (clientId && !searchParams.has('client_id')) {
|
|
32
|
+
searchParams.set('client_id', clientId);
|
|
33
|
+
}
|
|
34
|
+
if (postLogoutRedirectURI && !searchParams.has('post_logout_redirect_uri')) {
|
|
35
|
+
searchParams.set('post_logout_redirect_uri', postLogoutRedirectURI);
|
|
36
|
+
}
|
|
37
|
+
return providerLogOutURL.href;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async logOut() {
|
|
41
|
+
let socket = this.getSocket();
|
|
42
|
+
// The auth token must be read before the socket is deauthenticated.
|
|
43
|
+
let providerLogOutURL = this.getProviderLogOutURL(socket.authToken);
|
|
44
|
+
await socket.deauthenticate();
|
|
45
|
+
if (providerLogOutURL) {
|
|
46
|
+
location.href = providerLogOutURL;
|
|
47
|
+
}
|
|
6
48
|
}
|
|
7
49
|
}
|
|
8
50
|
|
package/package.json
CHANGED
package/switch-group.js
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
import { debouncer } from './utils.js';
|
|
2
|
-
|
|
3
|
-
const DEBOUNCE_DELAY = 0;
|
|
4
|
-
|
|
5
1
|
class SwitchGroup extends HTMLElement {
|
|
6
2
|
constructor() {
|
|
7
3
|
super();
|
|
8
4
|
this.isReady = false;
|
|
5
|
+
this.renderPending = false;
|
|
6
|
+
this.lastViewportNode = null;
|
|
7
|
+
this.lastRenderedContent = null;
|
|
9
8
|
this.attachShadow({ mode: 'open' });
|
|
10
9
|
this.shadowRoot.innerHTML = '<slot name="content"></slot><slot name="viewport"></slot>';
|
|
11
10
|
|
|
12
|
-
let debounce = debouncer();
|
|
13
|
-
|
|
14
11
|
this.processTemplates = () => {
|
|
15
12
|
let viewportSlot = this.shadowRoot.querySelector('slot[name="viewport"]');
|
|
16
13
|
let viewportNode = viewportSlot.assignedNodes()[0];
|
|
@@ -45,10 +42,33 @@ class SwitchGroup extends HTMLElement {
|
|
|
45
42
|
}
|
|
46
43
|
}
|
|
47
44
|
|
|
48
|
-
|
|
45
|
+
let content = contentList.join('');
|
|
46
|
+
|
|
47
|
+
// Rewriting innerHTML with identical markup recreates the nodes, which loses
|
|
48
|
+
// decoded images, focus and scroll position; greedy-refresh forces that reset.
|
|
49
|
+
if (
|
|
50
|
+
!this.hasAttribute('greedy-refresh') &&
|
|
51
|
+
viewportNode === this.lastViewportNode &&
|
|
52
|
+
content === this.lastRenderedContent
|
|
53
|
+
) return;
|
|
54
|
+
|
|
55
|
+
this.lastViewportNode = viewportNode;
|
|
56
|
+
this.lastRenderedContent = content;
|
|
57
|
+
|
|
58
|
+
viewportNode.innerHTML = content;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Coalescing on a microtask keeps the write ahead of the next paint; deferring
|
|
62
|
+
// with a timeout lets the browser paint the empty viewport first, which flickers.
|
|
63
|
+
this.debouncedProcessTemplates = () => {
|
|
64
|
+
if (this.renderPending) return;
|
|
65
|
+
this.renderPending = true;
|
|
66
|
+
queueMicrotask(() => {
|
|
67
|
+
this.renderPending = false;
|
|
68
|
+
this.processTemplates();
|
|
69
|
+
});
|
|
49
70
|
};
|
|
50
71
|
|
|
51
|
-
this.debouncedProcessTemplates = debounce(this.processTemplates, DEBOUNCE_DELAY);
|
|
52
72
|
this.shadowRoot.addEventListener('slotchange', this.debouncedProcessTemplates);
|
|
53
73
|
}
|
|
54
74
|
|