sol-components 2.1.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 +7 -0
- package/core/activate.js +27 -0
- package/core/adopt.js +71 -0
- package/core/auth-core.js +73 -0
- package/core/auth-fetch.js +154 -0
- package/core/component-mount.js +110 -0
- package/core/defaults.js +48 -0
- package/core/define.js +15 -0
- package/core/display-target.js +166 -0
- package/core/edit-placements.js +28 -0
- package/core/editor-self.js +127 -0
- package/core/editor.js +162 -0
- package/core/events.js +27 -0
- package/core/extension-points.js +189 -0
- package/core/form-utils.js +210 -0
- package/core/from-query.js +138 -0
- package/core/from-rdf.js +52 -0
- package/core/here.js +33 -0
- package/core/include-core.js +73 -0
- package/core/inrupt-global.js +18 -0
- package/core/menu-consumer.js +41 -0
- package/core/menu-rdf.js +154 -0
- package/core/pod-ops.js +392 -0
- package/core/pod-registry.js +82 -0
- package/core/popup-proxy.js +255 -0
- package/core/rdf-core.js +280 -0
- package/core/rdf-render.js +136 -0
- package/core/rdf-utils.js +411 -0
- package/core/rdf.js +154 -0
- package/core/services.js +106 -0
- package/core/shape-to-form.js +741 -0
- package/core/sparql-safety.js +20 -0
- package/core/utils.js +196 -0
- package/dist/importmap-cdn.json +49 -0
- package/dist/importmap-local.json +49 -0
- package/dist/sol-loader.manifest.json +140 -0
- package/dist/vendor/@comunica-query-sparql.js +137851 -0
- package/dist/vendor/@inrupt-solid-client-authn-browser.js +7503 -0
- package/dist/vendor/dompurify.js +1476 -0
- package/dist/vendor/ical.js.js +9739 -0
- package/dist/vendor/marked.js +85 -0
- package/dist/vendor/n3.js +14670 -0
- package/dist/vendor/rdf-validate-shacl.js +6970 -0
- package/dist/vendor/rdflib.js +35172 -0
- package/dist/vendor/solid-logic.js +6819 -0
- package/dist/vendor/solid-ui.js +21945 -0
- package/node/sol-form.js +133 -0
- package/node/sol-include.js +55 -0
- package/node/sol-login.js +632 -0
- package/node/sol-menu.js +639 -0
- package/node/sol-query.js +116 -0
- package/package.json +133 -0
- package/web/menu-from-rdf.js +23 -0
- package/web/scripts/prefs.js +25 -0
- package/web/sol-accordion.js +114 -0
- package/web/sol-basic.js +50 -0
- package/web/sol-breadcrumb.js +131 -0
- package/web/sol-button.js +244 -0
- package/web/sol-calendar.js +465 -0
- package/web/sol-default.js +118 -0
- package/web/sol-dropdown-button.js +222 -0
- package/web/sol-feed.js +1336 -0
- package/web/sol-form.js +949 -0
- package/web/sol-full.js +43 -0
- package/web/sol-gallery.js +303 -0
- package/web/sol-include.js +246 -0
- package/web/sol-live-edit.js +415 -0
- package/web/sol-login.js +856 -0
- package/web/sol-menu.js +593 -0
- package/web/sol-modal.js +377 -0
- package/web/sol-pod-extras.js +17 -0
- package/web/sol-pod-ops.js +680 -0
- package/web/sol-pod.js +1039 -0
- package/web/sol-query.js +546 -0
- package/web/sol-rolodex.js +95 -0
- package/web/sol-search.js +402 -0
- package/web/sol-settings.js +199 -0
- package/web/sol-solidos.js +93 -0
- package/web/sol-tabs.js +445 -0
- package/web/sol-time.js +194 -0
- package/web/sol-tree-edit.js +492 -0
- package/web/sol-wac.js +456 -0
- package/web/sol-weather.js +337 -0
- package/web/sol-window.js +142 -0
- package/web/styles/buttons-css.js +108 -0
- package/web/styles/help.css +242 -0
- package/web/styles/root.css +112 -0
- package/web/styles/sol-accordion-css.js +97 -0
- package/web/styles/sol-calendar-css.js +154 -0
- package/web/styles/sol-feed-css.js +475 -0
- package/web/styles/sol-form-css.js +471 -0
- package/web/styles/sol-gallery-css.js +181 -0
- package/web/styles/sol-include-css.js +95 -0
- package/web/styles/sol-live-edit-css.js +84 -0
- package/web/styles/sol-live-edit.css +101 -0
- package/web/styles/sol-login-css.js +116 -0
- package/web/styles/sol-menu-css.js +145 -0
- package/web/styles/sol-modal-css.js +134 -0
- package/web/styles/sol-pod-css.js +187 -0
- package/web/styles/sol-pod-modal-css.js +203 -0
- package/web/styles/sol-query-css.js +140 -0
- package/web/styles/sol-query-help.css +267 -0
- package/web/styles/sol-query-one-pager.css +67 -0
- package/web/styles/sol-search-css.js +157 -0
- package/web/styles/sol-solidos-css.js +7 -0
- package/web/styles/sol-tabs-css.js +114 -0
- package/web/styles/sol-time-css.js +30 -0
- package/web/styles/sol-wac-css.js +73 -0
- package/web/styles/sol-weather-css.js +59 -0
- package/web/styles/solid-logo.svg +9 -0
- package/web/styles/view-accordion-css.js +66 -0
- package/web/styles/view-anchorlist-css.js +22 -0
- package/web/styles/view-autocomplete-css.js +59 -0
- package/web/styles/view-rolodex-css.js +102 -0
- package/web/styles/view-select-css.js +21 -0
- package/web/utils/calendar-fetch.js +388 -0
- package/web/utils/code-mirror-editor.js +82 -0
- package/web/utils/commons-fetch.js +108 -0
- package/web/utils/feed-edit.js +159 -0
- package/web/utils/feed-edit.smoke.mjs +74 -0
- package/web/utils/feed-fetch.js +573 -0
- package/web/utils/live-edit-help/csv.js +64 -0
- package/web/utils/live-edit-help/graphviz.js +41 -0
- package/web/utils/live-edit-help/jsonld.js +55 -0
- package/web/utils/live-edit-help/markdown.js +52 -0
- package/web/utils/live-edit-help/mermaid.js +48 -0
- package/web/utils/live-edit-help/turtle.js +85 -0
- package/web/utils/rdf-config.js +125 -0
- package/web/utils/renderers/csv.js +124 -0
- package/web/utils/renderers/d3-force.js +82 -0
- package/web/utils/renderers/graphviz.js +13 -0
- package/web/utils/renderers/html.js +10 -0
- package/web/utils/renderers/jsonld.js +63 -0
- package/web/utils/renderers/markdown.js +19 -0
- package/web/utils/renderers/mermaid.js +54 -0
- package/web/utils/renderers/turtle.js +51 -0
- package/web/utils/sol-query-triple-patterns.js +151 -0
- package/web/utils/sol-query-ui.js +250 -0
- package/web/utils/sol-query-views.js +32 -0
- package/web/views/_helpers.js +34 -0
- package/web/views/accordion.js +133 -0
- package/web/views/anchorlist.js +59 -0
- package/web/views/auto-complete.js +183 -0
- package/web/views/dl.js +38 -0
- package/web/views/list.js +19 -0
- package/web/views/menu.js +56 -0
- package/web/views/rolodex.js +126 -0
- package/web/views/select.js +79 -0
- package/web/views/table.js +73 -0
- package/web/views/tabs.js +57 -0
package/web/sol-login.js
ADDED
|
@@ -0,0 +1,856 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <sol-login> — Solid OIDC login web component.
|
|
3
|
+
* Attributes: issuers (comma-separated list of known issuer origins)
|
|
4
|
+
* Properties: fetchFor(url) — authenticated fetch, webId, isLoggedIn, session
|
|
5
|
+
* Events: sol-login({webId, issuer}), sol-logout
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* <sol-login></sol-login>
|
|
9
|
+
* <sol-login issuers="https://solidcommunity.net,https://login.inrupt.com"></sol-login>
|
|
10
|
+
*
|
|
11
|
+
* Expects @inrupt/solid-client-authn-browser loaded as UMD at window.solidClientAuthn
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { CSS, sheet as LOGIN_SHEET } from './styles/sol-login-css.js';
|
|
15
|
+
import { adopt } from '../core/adopt.js';
|
|
16
|
+
import { define } from '../core/define.js';
|
|
17
|
+
import { rdf } from '../core/rdf.js';
|
|
18
|
+
import {
|
|
19
|
+
originOf,
|
|
20
|
+
baseDomain,
|
|
21
|
+
sessionCoversOrigin,
|
|
22
|
+
isNoAuth as _isNoAuth,
|
|
23
|
+
getSessionFor as _getSessionFor,
|
|
24
|
+
makeFetchFor,
|
|
25
|
+
isLoggedInFor,
|
|
26
|
+
getWebId as _getWebId,
|
|
27
|
+
getFirstLoggedIn as _getFirstLoggedIn,
|
|
28
|
+
} from '../core/auth-core.js';
|
|
29
|
+
import { PopupProxySession } from '../core/popup-proxy.js';
|
|
30
|
+
import { solFetch } from '../core/auth-fetch.js';
|
|
31
|
+
import { register as registerService, root as swcRoot } from '../core/services.js';
|
|
32
|
+
|
|
33
|
+
document.addEventListener('DOMContentLoaded', async () => {
|
|
34
|
+
const login = document.querySelector('sol-login');
|
|
35
|
+
if (login && !login._manualInit) await login.initialize();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
function getSessionClass() {
|
|
40
|
+
const locations = [
|
|
41
|
+
window.solidClientAuthn?.Session,
|
|
42
|
+
window.solidClientAuthentication?.Session,
|
|
43
|
+
window.SolidClientAuthn?.Session,
|
|
44
|
+
window['@inrupt/solid-client-authn-browser']?.Session
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
for (const SessionClass of locations) {
|
|
48
|
+
if (SessionClass) return SessionClass;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
throw new Error('sol-login: solid-client-authn-browser must be loaded as UMD bundle. Expected at window.solidClientAuthn.Session or window.solidClientAuthentication.Session');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class AuthManager {
|
|
55
|
+
/** The page-wide singleton. Every `<sol-login>` instance binds to
|
|
56
|
+
* this same AuthManager so sessions established by any embedded app
|
|
57
|
+
* (podz left/right, future apps) are visible to shell-level code
|
|
58
|
+
* without DOM probing. */
|
|
59
|
+
static get shared() { return sharedAuth; }
|
|
60
|
+
|
|
61
|
+
constructor() {
|
|
62
|
+
this.sessions = new Map();
|
|
63
|
+
this._noAuth = null;
|
|
64
|
+
try {
|
|
65
|
+
this._sideOrigins = JSON.parse(localStorage.getItem('solLoginOrigins') || '{}');
|
|
66
|
+
} catch (e) { this._sideOrigins = {}; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
set noAuth(v) {
|
|
70
|
+
this._noAuth = v;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_noAuthConfig() {
|
|
74
|
+
return this._noAuth ?? window.SolidAppContext?.noAuth;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
isNoAuth(url) {
|
|
78
|
+
return _isNoAuth(url, this._noAuthConfig());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
originOf(url) { return originOf(url); }
|
|
82
|
+
|
|
83
|
+
_sessionId(tag, origin) {
|
|
84
|
+
return `sol_${tag}_${origin.replace(/[^a-z0-9]/gi, '_')}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_makeSession(sessionId) {
|
|
88
|
+
const SessionClass = getSessionClass();
|
|
89
|
+
return new SessionClass({}, sessionId);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
sessionFor(tag, origin) {
|
|
93
|
+
if (this.sessions.has(tag)) return this.sessions.get(tag);
|
|
94
|
+
const org = origin || this._sideOrigins[tag];
|
|
95
|
+
const sessionId = org ? this._sessionId(tag, org) : `sol_${tag}_unset`;
|
|
96
|
+
const session = this._makeSession(sessionId);
|
|
97
|
+
this.sessions.set(tag, session);
|
|
98
|
+
return session;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
setSideOrigin(tag, url) {
|
|
102
|
+
if (this.isNoAuth(url)) return;
|
|
103
|
+
const origin = this.originOf(url);
|
|
104
|
+
if (this._sideOrigins[tag] === origin) return;
|
|
105
|
+
const existing = this.sessions.get(tag);
|
|
106
|
+
if (existing && this._sessionCoversOrigin(existing, origin)) {
|
|
107
|
+
this._sideOrigins[tag] = origin;
|
|
108
|
+
this._persistOrigins();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this._sideOrigins[tag] = origin;
|
|
112
|
+
this._persistOrigins();
|
|
113
|
+
const sessionId = this._sessionId(tag, origin);
|
|
114
|
+
this.sessions.set(tag, this._makeSession(sessionId));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
_persistOrigins() {
|
|
118
|
+
try { localStorage.setItem('solLoginOrigins', JSON.stringify(this._sideOrigins)); } catch (e) {}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_sessionCoversOrigin(session, origin) {
|
|
122
|
+
return sessionCoversOrigin(session, origin);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
getSessionFor(url, tag) {
|
|
126
|
+
return _getSessionFor(this.sessions, url, tag, this._noAuthConfig());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fetchFor(url, tag) {
|
|
130
|
+
return makeFetchFor(this.sessions, url, tag, this._noAuthConfig(), fetch);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
isLoggedIn(url, tag) {
|
|
134
|
+
return isLoggedInFor(this.sessions, url, tag, this._noAuthConfig());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
getWebId(tag) {
|
|
138
|
+
return _getWebId(this.sessions, tag);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
getFirstLoggedIn() {
|
|
142
|
+
return _getFirstLoggedIn(this.sessions);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async handleIncomingRedirect() {
|
|
146
|
+
const pendingTag = localStorage.getItem('solLoginPendingTag');
|
|
147
|
+
localStorage.removeItem('solLoginPendingTag');
|
|
148
|
+
|
|
149
|
+
// Ensure the session that initiated login exists so it can process the redirect.
|
|
150
|
+
if (pendingTag) {
|
|
151
|
+
this.sessionFor(pendingTag);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const [, session] of this.sessions) {
|
|
155
|
+
await session.handleIncomingRedirect(window.location.href);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async ensureAuthenticated(url, tag = 'default') {
|
|
160
|
+
if (this.isNoAuth(url)) return true;
|
|
161
|
+
const origin = this.originOf(url);
|
|
162
|
+
this.setSideOrigin(tag, url);
|
|
163
|
+
const session = this.sessionFor(tag, origin);
|
|
164
|
+
if (session.info.isLoggedIn) return true;
|
|
165
|
+
|
|
166
|
+
for (const [, s] of this.sessions) {
|
|
167
|
+
if (sessionCoversOrigin(s, origin)) return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try { localStorage.setItem('solLoginPendingTag', tag); } catch (e) {}
|
|
171
|
+
const redirectUrl = window.location.origin + window.location.pathname;
|
|
172
|
+
await session.login({ oidcIssuer: origin, redirectUrl, clientName: 'Solid App' });
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// All <sol-login> instances on a page share one AuthManager so that
|
|
178
|
+
// podz's per-side login elements register into a single session Map.
|
|
179
|
+
// Single-login pages are unaffected (one consumer of the singleton).
|
|
180
|
+
const sharedAuth = new AuthManager();
|
|
181
|
+
|
|
182
|
+
// Publish auth to the ecosystem: expose AuthManager on the global (consumers in
|
|
183
|
+
// core/auth-fetch.js + web/sol-include.js already READ window.SolidWebComponents
|
|
184
|
+
// .AuthManager but nothing assigned it — this closes that gap), and register the
|
|
185
|
+
// `auth` host-service so any component reaches the signed-in fetch via
|
|
186
|
+
// window.SolidWebComponents.{auth,fetch} without importing swc.
|
|
187
|
+
try {
|
|
188
|
+
swcRoot().AuthManager = AuthManager;
|
|
189
|
+
registerService('auth', {
|
|
190
|
+
manager: sharedAuth,
|
|
191
|
+
fetch: solFetch,
|
|
192
|
+
fetchFor: (url, tag) => sharedAuth.fetchFor(url, tag),
|
|
193
|
+
});
|
|
194
|
+
} catch (_) { /* no window / pre-registration race — harmless */ }
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Solid OIDC login web component.
|
|
198
|
+
*
|
|
199
|
+
* Shows a log-in/log-out button with issuer dropdown. Manages OIDC sessions
|
|
200
|
+
* via @inrupt/solid-client-authn-browser and provides authenticated fetch.
|
|
201
|
+
*
|
|
202
|
+
* Two modes (the `mode` attribute):
|
|
203
|
+
* - "redirect" (default) — classic full-page OIDC redirect. One session
|
|
204
|
+
* per page. Unchanged behavior for existing consumers.
|
|
205
|
+
* - "popup" — login happens in a popup window that holds the real
|
|
206
|
+
* Session; the parent talks to it via a PopupProxySession. Lets
|
|
207
|
+
* multiple <sol-login side="..."> elements hold independent sessions
|
|
208
|
+
* in one tab. See core/popup-proxy.js and popup-auth-callback.html.
|
|
209
|
+
*
|
|
210
|
+
* @class SolLogin
|
|
211
|
+
* @extends HTMLElement
|
|
212
|
+
* @attr {string} issuers - comma-separated list of known OIDC issuer origins
|
|
213
|
+
* @attr {string} mode - "redirect" (default) | "popup"
|
|
214
|
+
* @attr {string} side - session tag for this element (popup mode); default "default"
|
|
215
|
+
* @attr {string} popup-callback - URL of the popup callback page (popup mode)
|
|
216
|
+
* @attr {string} external-auth - set automatically when another same-origin
|
|
217
|
+
* window/tab/iframe reports a login over BroadcastChannel
|
|
218
|
+
* ('sol-auth'). Pure visual signal — CSS in sol-login-css.js
|
|
219
|
+
* paints the button green and surfaces the chip so the user
|
|
220
|
+
* can choose to also log in here. Cleared when this element
|
|
221
|
+
* holds its own session.
|
|
222
|
+
*
|
|
223
|
+
* Cross-window auth signaling: every sol-login opens a
|
|
224
|
+
* BroadcastChannel('sol-auth') in connectedCallback. Own login/logout events
|
|
225
|
+
* are rebroadcast on the channel; foreign login messages set the
|
|
226
|
+
* `external-auth` attribute (and clear it on logout) — unless this element
|
|
227
|
+
* already has its own logged-in session, in which case foreign signals are
|
|
228
|
+
* ignored. A `hello` ping on connect asks existing windows to announce
|
|
229
|
+
* themselves so newly-mounted elements pick up state established before
|
|
230
|
+
* they existed. Used by dk-solidos's iframe (broadcasts mashlib's session
|
|
231
|
+
* via pages/solidos-host.html) and any other same-origin sol-login.
|
|
232
|
+
*
|
|
233
|
+
* @property {Function} fetchFor - fetchFor(url) returns authenticated fetch
|
|
234
|
+
* @property {string} webId - logged-in user's WebID
|
|
235
|
+
* @property {boolean} isLoggedIn - whether a session is active
|
|
236
|
+
* @fires sol-login - detail: { webId, issuer, side }
|
|
237
|
+
* @fires sol-logout - detail: { side }
|
|
238
|
+
*/
|
|
239
|
+
class SolLogin extends HTMLElement {
|
|
240
|
+
static get observedAttributes() { return ['issuers', 'mode', 'side', 'popup-callback']; }
|
|
241
|
+
|
|
242
|
+
constructor() {
|
|
243
|
+
super();
|
|
244
|
+
this.attachShadow({ mode: 'open' });
|
|
245
|
+
this._auth = sharedAuth;
|
|
246
|
+
this._issuers = [];
|
|
247
|
+
this._initialized = false;
|
|
248
|
+
this._mode = 'redirect';
|
|
249
|
+
this._side = 'default';
|
|
250
|
+
this._popupCallback = './popup-auth-callback.html';
|
|
251
|
+
this._popupWindow = null;
|
|
252
|
+
this._popupMsgHandler = null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
get auth() { return this._auth; }
|
|
256
|
+
|
|
257
|
+
/** The session for this element's side (popup mode), if any. */
|
|
258
|
+
_sideSession() {
|
|
259
|
+
return this._auth.sessions.get(this._side) || null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
get webId() {
|
|
263
|
+
const s = this._mode === 'popup'
|
|
264
|
+
? this._sideSession()
|
|
265
|
+
: this._auth.getFirstLoggedIn();
|
|
266
|
+
return s?.info?.webId || null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
get isLoggedIn() {
|
|
270
|
+
if (this._mode === 'popup') {
|
|
271
|
+
return !!this._sideSession()?.info?.isLoggedIn;
|
|
272
|
+
}
|
|
273
|
+
return !!this._auth.getFirstLoggedIn();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
fetchFor(url, tag) {
|
|
277
|
+
return this._auth.fetchFor(url, tag);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
set issuers(arr) {
|
|
281
|
+
this._issuers = arr || [];
|
|
282
|
+
if (this.isConnected) this._renderIssuers();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
get issuers() { return this._issuers; }
|
|
286
|
+
|
|
287
|
+
addIssuer(origin) {
|
|
288
|
+
try {
|
|
289
|
+
const o = new URL(origin).origin;
|
|
290
|
+
if (!this._issuers.includes(o)) {
|
|
291
|
+
this._issuers.push(o);
|
|
292
|
+
if (this.isConnected) this._renderIssuers();
|
|
293
|
+
}
|
|
294
|
+
} catch (e) {}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
connectedCallback() {
|
|
298
|
+
if (!this._initialized) {
|
|
299
|
+
this._initialized = true;
|
|
300
|
+
this._mode = (this.getAttribute('mode') || 'redirect').toLowerCase();
|
|
301
|
+
this._side = this.getAttribute('side') || 'default';
|
|
302
|
+
const cb = this.getAttribute('popup-callback');
|
|
303
|
+
if (cb) this._popupCallback = cb;
|
|
304
|
+
this._render();
|
|
305
|
+
const attr = this.getAttribute('issuers');
|
|
306
|
+
if (attr) this._issuers = attr.split(',').map(s => s.trim()).filter(Boolean);
|
|
307
|
+
// Self-listeners broadcast our own login/logout to the
|
|
308
|
+
// same-origin BroadcastChannel so other windows / iframes light
|
|
309
|
+
// up their login button green ('external-auth' attribute).
|
|
310
|
+
this.addEventListener('sol-login', (e) => this._broadcastLogin(e));
|
|
311
|
+
this.addEventListener('sol-logout', () => this._broadcastLogout());
|
|
312
|
+
}
|
|
313
|
+
this._attachAuthNeededListener();
|
|
314
|
+
this._setupAuthChannel();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
disconnectedCallback() {
|
|
318
|
+
if (this._popupMsgHandler) {
|
|
319
|
+
window.removeEventListener('message', this._popupMsgHandler);
|
|
320
|
+
this._popupMsgHandler = null;
|
|
321
|
+
}
|
|
322
|
+
this._detachAuthNeededListener();
|
|
323
|
+
if (this._authChannel) {
|
|
324
|
+
this._authChannel.close();
|
|
325
|
+
this._authChannel = null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/* ── cross-window auth signaling ───────────────────────────────────
|
|
330
|
+
* BroadcastChannel('sol-auth') carries login/logout across every
|
|
331
|
+
* same-origin tab, window, and iframe (including mashlib in the
|
|
332
|
+
* dk-solidos iframe, which broadcasts via pages/solidos-host.html).
|
|
333
|
+
* On receipt of a foreign login, if THIS sol-login has no own
|
|
334
|
+
* logged-in session, we set the `external-auth` attribute — pure
|
|
335
|
+
* CSS in sol-login-css.js paints the button green to invite the
|
|
336
|
+
* user to log in here too. */
|
|
337
|
+
_setupAuthChannel() {
|
|
338
|
+
if (this._authChannel) return;
|
|
339
|
+
if (typeof BroadcastChannel === 'undefined') return;
|
|
340
|
+
try { this._authChannel = new BroadcastChannel('sol-auth'); }
|
|
341
|
+
catch (_) { return; }
|
|
342
|
+
this._authChannel.addEventListener('message', (e) => this._onAuthMessage(e));
|
|
343
|
+
// Ask any already-logged-in window to announce itself, so our
|
|
344
|
+
// newly-connected element doesn't miss state established before
|
|
345
|
+
// we mounted.
|
|
346
|
+
try { this._authChannel.postMessage({ type: 'hello' }); } catch (_) {}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
_broadcastLogin(e) {
|
|
350
|
+
if (!this._authChannel) return;
|
|
351
|
+
try {
|
|
352
|
+
this._authChannel.postMessage({
|
|
353
|
+
type: 'login',
|
|
354
|
+
webId: e?.detail?.webId,
|
|
355
|
+
side: e?.detail?.side ?? this._side,
|
|
356
|
+
});
|
|
357
|
+
} catch (_) {}
|
|
358
|
+
// We're now the source; clear any prior external-auth on self.
|
|
359
|
+
this.removeAttribute('external-auth');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
_broadcastLogout() {
|
|
363
|
+
if (!this._authChannel) return;
|
|
364
|
+
try { this._authChannel.postMessage({ type: 'logout' }); } catch (_) {}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
_onAuthMessage(e) {
|
|
368
|
+
const d = e?.data;
|
|
369
|
+
if (!d || typeof d !== 'object') return;
|
|
370
|
+
if (d.type === 'hello') {
|
|
371
|
+
// Reply to newcomers if we hold a session.
|
|
372
|
+
const s = this._auth.getFirstLoggedIn();
|
|
373
|
+
if (s && this._authChannel) {
|
|
374
|
+
try {
|
|
375
|
+
this._authChannel.postMessage({
|
|
376
|
+
type: 'login', webId: s.info?.webId, side: this._side,
|
|
377
|
+
});
|
|
378
|
+
} catch (_) {}
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (d.type === 'login') {
|
|
383
|
+
if (this._auth.getFirstLoggedIn()) return;
|
|
384
|
+
this.setAttribute('external-auth', d.webId || '');
|
|
385
|
+
} else if (d.type === 'logout') {
|
|
386
|
+
// Conservative: clear immediately. If a different window is
|
|
387
|
+
// still logged in, its next 'hello' / 'login' event repaints.
|
|
388
|
+
this.removeAttribute('external-auth');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/* ── sol-auth-needed listener ──────────────────────────────────────
|
|
393
|
+
* Components save through solFetch (core/auth-fetch.js); when a
|
|
394
|
+
* request returns 401, solFetch dispatches `sol-auth-needed` and
|
|
395
|
+
* waits for someone to resolve its detail promise. We listen on
|
|
396
|
+
* `document`, pick the default issuer (own `issuer` attribute, then
|
|
397
|
+
* `<sol-default default-issuer>`, then the first entry in our list),
|
|
398
|
+
* run the existing login flow, and resolve the promise on success or
|
|
399
|
+
* give-up.
|
|
400
|
+
*
|
|
401
|
+
* Concurrent prompts are coalesced — multiple solFetch callers that
|
|
402
|
+
* hit 401 in the same window will share one login attempt rather
|
|
403
|
+
* than stacking popups.
|
|
404
|
+
*/
|
|
405
|
+
|
|
406
|
+
_attachAuthNeededListener() {
|
|
407
|
+
if (this._authNeededHandler) return;
|
|
408
|
+
this._authNeededHandler = (e) => this._handleAuthNeeded(e);
|
|
409
|
+
document.addEventListener('sol-auth-needed', this._authNeededHandler);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
_detachAuthNeededListener() {
|
|
413
|
+
if (this._authNeededHandler) {
|
|
414
|
+
document.removeEventListener('sol-auth-needed', this._authNeededHandler);
|
|
415
|
+
this._authNeededHandler = null;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
_resolveDefaultIssuer() {
|
|
420
|
+
return this.getAttribute('issuer')
|
|
421
|
+
|| (document.querySelector('sol-default')?.getAttribute('default-issuer'))
|
|
422
|
+
|| this._issuers[0]
|
|
423
|
+
|| null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async _handleAuthNeeded(e) {
|
|
427
|
+
const { resolve, reject, side } = e.detail || {};
|
|
428
|
+
if (typeof resolve !== 'function') return;
|
|
429
|
+
|
|
430
|
+
// Side-scoped routing: if the event names a side, only the matching
|
|
431
|
+
// <sol-login> handles it. Untagged events (no side) fall through to
|
|
432
|
+
// every listener — first to settle wins, others no-op. This keeps
|
|
433
|
+
// left/right pod chips from both popping their dropdowns for the
|
|
434
|
+
// same authTag-bearing 401.
|
|
435
|
+
if (side && side !== this._side) return;
|
|
436
|
+
|
|
437
|
+
if (this._pendingAuthPromise) {
|
|
438
|
+
try { resolve(await this._pendingAuthPromise); }
|
|
439
|
+
catch (err) { reject?.(err); }
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const issuer = this._resolveDefaultIssuer();
|
|
444
|
+
if (!issuer) { resolve(false); return; }
|
|
445
|
+
|
|
446
|
+
// Surface the element for the duration of the auth flow so the
|
|
447
|
+
// user can pick a different issuer (the picker dropdown lives in
|
|
448
|
+
// sol-login's own UI). `active` is the CSS hook in
|
|
449
|
+
// styles/sol-login-css.js — :host([active]) flips display back on.
|
|
450
|
+
this.setAttribute('active', '');
|
|
451
|
+
|
|
452
|
+
// Open the dropdown so the issuer list is visible while auto-login
|
|
453
|
+
// is running. The user can click a different issuer to switch
|
|
454
|
+
// (which closes the in-flight popup and opens a fresh one — see
|
|
455
|
+
// _popupLogin's reissue handling).
|
|
456
|
+
requestAnimationFrame(() => {
|
|
457
|
+
this._showSwitchHint(issuer);
|
|
458
|
+
this._toggleDropdown();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
this._pendingAuthPromise = new Promise((res) => {
|
|
462
|
+
const cleanup = () => {
|
|
463
|
+
this.removeEventListener('sol-login', onLogin);
|
|
464
|
+
this.removeEventListener('sol-popup-blocked', onFail);
|
|
465
|
+
this.removeAttribute('active');
|
|
466
|
+
this._closeDropdown();
|
|
467
|
+
this._hideSwitchHint();
|
|
468
|
+
this._pendingAuthPromise = null;
|
|
469
|
+
};
|
|
470
|
+
const onLogin = () => { cleanup(); res(true); };
|
|
471
|
+
const onFail = () => { cleanup(); res(false); };
|
|
472
|
+
this.addEventListener('sol-login', onLogin);
|
|
473
|
+
this.addEventListener('sol-popup-blocked', onFail);
|
|
474
|
+
Promise.resolve(this.login(issuer)).catch(() => onFail());
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
try { resolve(await this._pendingAuthPromise); }
|
|
478
|
+
catch (err) { reject?.(err); }
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
attributeChangedCallback(name, oldV, newV) {
|
|
482
|
+
if (oldV === newV) return;
|
|
483
|
+
if (name === 'issuers' && this._initialized) {
|
|
484
|
+
this._issuers = (newV || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
485
|
+
this._renderIssuers();
|
|
486
|
+
} else if (name === 'mode' && this._initialized) {
|
|
487
|
+
this._mode = (newV || 'redirect').toLowerCase();
|
|
488
|
+
} else if (name === 'side' && this._initialized) {
|
|
489
|
+
this._side = newV || 'default';
|
|
490
|
+
this._updateUI();
|
|
491
|
+
} else if (name === 'popup-callback' && newV) {
|
|
492
|
+
this._popupCallback = newV;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async login(issuerUrl, tag = 'default') {
|
|
497
|
+
if (this._mode === 'popup') {
|
|
498
|
+
return this._popupLogin(issuerUrl);
|
|
499
|
+
}
|
|
500
|
+
await this._auth.ensureAuthenticated(issuerUrl, tag);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Popup-mode login. Opens the callback page in a popup that runs the
|
|
505
|
+
* OIDC redirect on its own; when it posts back `logged-in`, we wrap the
|
|
506
|
+
* popup in a PopupProxySession and register it under this element's side.
|
|
507
|
+
*/
|
|
508
|
+
_popupLogin(issuerUrl) {
|
|
509
|
+
let issuer = issuerUrl;
|
|
510
|
+
try { issuer = new URL(issuerUrl).href; } catch (e) {
|
|
511
|
+
this._setStatusMessage('Invalid issuer URL', true);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Reuse vs. reissue: if a popup is already open for this side and
|
|
516
|
+
// the issuer matches, just refocus. If the caller is switching to
|
|
517
|
+
// a different issuer (e.g. user clicked another option in the
|
|
518
|
+
// dropdown while auto-login was in flight), close the old popup
|
|
519
|
+
// and open a fresh one with the new issuer URL.
|
|
520
|
+
if (this._popupWindow && !this._popupWindow.closed) {
|
|
521
|
+
if (this._popupIssuer === issuer) {
|
|
522
|
+
this._popupWindow.focus();
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
try { this._popupWindow.close(); } catch (e) {}
|
|
526
|
+
this._popupWindow = null;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const url = this._popupCallback +
|
|
530
|
+
(this._popupCallback.includes('?') ? '&' : '?') +
|
|
531
|
+
'side=' + encodeURIComponent(this._side) +
|
|
532
|
+
'&issuer=' + encodeURIComponent(issuer);
|
|
533
|
+
const features = 'popup=yes,width=480,height=620';
|
|
534
|
+
const w = window.open(url, 'sol-login-' + this._side, features);
|
|
535
|
+
if (!w) {
|
|
536
|
+
this._setStatusMessage('Popup blocked — allow popups and retry', true);
|
|
537
|
+
this.dispatchEvent(new CustomEvent('sol-popup-blocked', {
|
|
538
|
+
bubbles: true, composed: true, detail: { side: this._side },
|
|
539
|
+
}));
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
this._popupWindow = w;
|
|
543
|
+
this._popupIssuer = issuer;
|
|
544
|
+
this._setStatusMessage('Signing in…');
|
|
545
|
+
// Auto-login also wants the hint updated as the user re-picks.
|
|
546
|
+
if (this.hasAttribute('active')) this._showSwitchHint(issuer);
|
|
547
|
+
|
|
548
|
+
if (!this._popupMsgHandler) {
|
|
549
|
+
this._popupMsgHandler = (e) => this._onPopupMessage(e);
|
|
550
|
+
window.addEventListener('message', this._popupMsgHandler);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
_onPopupMessage(e) {
|
|
555
|
+
const d = e.data;
|
|
556
|
+
if (!d || d.source !== 'sol-popup-auth') return;
|
|
557
|
+
if (d.side && d.side !== this._side) return;
|
|
558
|
+
|
|
559
|
+
if (d.type === 'logged-in') {
|
|
560
|
+
const proxy = new PopupProxySession(this._popupWindow, {
|
|
561
|
+
webId: d.webId, sessionId: d.sessionId, issuer: d.issuer,
|
|
562
|
+
clientId: null, side: this._side,
|
|
563
|
+
}, window.location.origin);
|
|
564
|
+
proxy.addEventListener('logout', () => {
|
|
565
|
+
if (this._auth.sessions.get(this._side) === proxy) {
|
|
566
|
+
this._auth.sessions.delete(this._side);
|
|
567
|
+
}
|
|
568
|
+
this._popupWindow = null;
|
|
569
|
+
this._updateUI();
|
|
570
|
+
this.dispatchEvent(new CustomEvent('sol-logout', {
|
|
571
|
+
bubbles: true, composed: true, detail: { side: this._side },
|
|
572
|
+
}));
|
|
573
|
+
});
|
|
574
|
+
this._auth.sessions.set(this._side, proxy);
|
|
575
|
+
this._updateUI();
|
|
576
|
+
this.dispatchEvent(new CustomEvent('sol-login', {
|
|
577
|
+
bubbles: true, composed: true,
|
|
578
|
+
detail: { webId: d.webId, issuer: d.issuer, side: this._side },
|
|
579
|
+
}));
|
|
580
|
+
this._integrateWithRdflib();
|
|
581
|
+
} else if (d.type === 'login-failed') {
|
|
582
|
+
this._popupWindow = null;
|
|
583
|
+
this._setStatusMessage('Sign-in failed', true);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async initialize(tags = ['default']) {
|
|
588
|
+
if (this._mode === 'popup') {
|
|
589
|
+
// PR 1: no cross-reload persistence — nothing to restore on boot.
|
|
590
|
+
this._updateUI();
|
|
591
|
+
this._integrateWithRdflib();
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
for (const tag of tags) {
|
|
595
|
+
this._auth.sessionFor(tag);
|
|
596
|
+
}
|
|
597
|
+
await this._auth.handleIncomingRedirect();
|
|
598
|
+
this._updateUI();
|
|
599
|
+
this._integrateWithRdflib();
|
|
600
|
+
|
|
601
|
+
const firstSession = this._auth.getFirstLoggedIn();
|
|
602
|
+
if (firstSession) {
|
|
603
|
+
this.dispatchEvent(new CustomEvent('sol-login', {
|
|
604
|
+
bubbles: true, composed: true,
|
|
605
|
+
detail: {
|
|
606
|
+
webId: firstSession.info.webId,
|
|
607
|
+
issuer: firstSession.info.issuer
|
|
608
|
+
}
|
|
609
|
+
}));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async logout() {
|
|
614
|
+
if (this._mode === 'popup') {
|
|
615
|
+
// Log out only this element's side.
|
|
616
|
+
const session = this._sideSession();
|
|
617
|
+
if (session) {
|
|
618
|
+
try { await session.logout(); } catch (e) {}
|
|
619
|
+
this._auth.sessions.delete(this._side);
|
|
620
|
+
}
|
|
621
|
+
this._popupWindow = null;
|
|
622
|
+
this._updateUI();
|
|
623
|
+
this._integrateWithRdflib();
|
|
624
|
+
this.dispatchEvent(new CustomEvent('sol-logout', {
|
|
625
|
+
bubbles: true, composed: true, detail: { side: this._side },
|
|
626
|
+
}));
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
for (const [, session] of this._auth.sessions) {
|
|
630
|
+
if (session.info?.isLoggedIn) {
|
|
631
|
+
await session.logout();
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
this._updateUI();
|
|
635
|
+
this._integrateWithRdflib();
|
|
636
|
+
this.dispatchEvent(new CustomEvent('sol-logout', { bubbles: true, composed: true }));
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
_integrateWithRdflib() {
|
|
640
|
+
const win = typeof window !== 'undefined' ? window : {};
|
|
641
|
+
|
|
642
|
+
// Route rdflib's Fetcher (and anything else we patch) through solFetch
|
|
643
|
+
// so a 401 from UpdateManager-driven saves (sol-form), sol-query SPARQL
|
|
644
|
+
// calls, sol-include document loads, etc. triggers `sol-auth-needed`
|
|
645
|
+
// and gets the chrome's login UX + auto-retry. solFetch internally
|
|
646
|
+
// calls am.fetchFor under the hood, so an already-authenticated
|
|
647
|
+
// request still goes through the right session.
|
|
648
|
+
const authFetchWrapper = (uri, options = {}) => solFetch(uri, options);
|
|
649
|
+
|
|
650
|
+
const patchFetcherCtor = (FetcherCtor) => {
|
|
651
|
+
if (!FetcherCtor?.prototype) return;
|
|
652
|
+
const proto = FetcherCtor.prototype;
|
|
653
|
+
if (!proto._originalFetch) {
|
|
654
|
+
proto._originalFetch = proto.fetch || proto._fetch || fetch;
|
|
655
|
+
}
|
|
656
|
+
if (proto.fetch) proto.fetch = authFetchWrapper;
|
|
657
|
+
if (proto._fetch) proto._fetch = authFetchWrapper;
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
const patchFetcherInstance = (fetcher) => {
|
|
661
|
+
if (!fetcher) return;
|
|
662
|
+
if (!fetcher._originalFetch) {
|
|
663
|
+
fetcher._originalFetch = fetcher.fetch || fetcher._fetch || fetch;
|
|
664
|
+
}
|
|
665
|
+
if (fetcher.fetch) fetcher.fetch = authFetchWrapper;
|
|
666
|
+
if (fetcher._fetch) fetcher._fetch = authFetchWrapper;
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
// 1. Patch Fetcher constructors (host-page global + our singleton) so any
|
|
670
|
+
// future `new Fetcher(...)` call gets auth.
|
|
671
|
+
patchFetcherCtor(win.$rdf?.Fetcher);
|
|
672
|
+
if (rdf.isReady()) patchFetcherCtor(rdf.Fetcher);
|
|
673
|
+
|
|
674
|
+
// 2. Adopt an external shared store if one is already on the page. This
|
|
675
|
+
// makes our components and solid-logic / solid-ui / mashlib share one
|
|
676
|
+
// rdflib graph (same cache, same subscriptions), so data loaded by
|
|
677
|
+
// either side is visible to the other.
|
|
678
|
+
// Probes solid-logic (`window.SolidLogic.store`), solid-ui / mashlib
|
|
679
|
+
// (`window.UI.store`), and the older `window.panes.store` surface.
|
|
680
|
+
const externalStore =
|
|
681
|
+
win.SolidLogic?.store
|
|
682
|
+
|| win.UI?.store
|
|
683
|
+
|| win.panes?.store
|
|
684
|
+
|| null;
|
|
685
|
+
if (externalStore && rdf.isReady()) rdf.useStore(externalStore);
|
|
686
|
+
|
|
687
|
+
// 2b. If nothing was on the page, publish our singleton upward so
|
|
688
|
+
// mashlib/solid-ui/solid-logic loaded *after* us share our graph.
|
|
689
|
+
if (!externalStore && rdf.isReady() && !win.SolidLogic) {
|
|
690
|
+
win.SolidLogic = { store: rdf.store, fetcher: rdf.storeFetcher };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// 3. Patch any already-instantiated Fetcher instances hanging off the
|
|
694
|
+
// shared store(s), so existing rdflib code paths also get auth.
|
|
695
|
+
patchFetcherInstance(win.SolidLogic?.store?.fetcher);
|
|
696
|
+
patchFetcherInstance(win.UI?.store?.fetcher);
|
|
697
|
+
patchFetcherInstance(win.panes?.store?.fetcher);
|
|
698
|
+
if (rdf.isReady()) patchFetcherInstance(rdf._fetcher);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
_render() {
|
|
702
|
+
const s = this.shadowRoot;
|
|
703
|
+
s.innerHTML = `
|
|
704
|
+
<span class="auth-status"></span>
|
|
705
|
+
<button class="sol-btn sol-btn-sm sol-btn-primary auth-btn">Log in</button>
|
|
706
|
+
<div class="dropdown">
|
|
707
|
+
<div class="issuer-list"></div>
|
|
708
|
+
<div class="custom-row">
|
|
709
|
+
<input class="sol-input issuer-input" type="text" placeholder="https://your-issuer.org">
|
|
710
|
+
<button class="sol-btn sol-btn-sm sol-btn-primary">Log in</button>
|
|
711
|
+
</div>
|
|
712
|
+
</div>`;
|
|
713
|
+
s.adoptedStyleSheets = [];
|
|
714
|
+
adopt(s, { sheet: LOGIN_SHEET, css: CSS });
|
|
715
|
+
|
|
716
|
+
const mainBtn = s.querySelector('.auth-btn');
|
|
717
|
+
mainBtn.addEventListener('click', () => this._handleClick());
|
|
718
|
+
|
|
719
|
+
const goBtn = s.querySelector('.custom-row .sol-btn');
|
|
720
|
+
goBtn.addEventListener('click', () => this._loginCustom());
|
|
721
|
+
|
|
722
|
+
const input = s.querySelector('.issuer-input');
|
|
723
|
+
input.addEventListener('keydown', (e) => {
|
|
724
|
+
if (e.key === 'Enter') this._loginCustom();
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
_handleClick() {
|
|
729
|
+
if (this.isLoggedIn) {
|
|
730
|
+
this.logout();
|
|
731
|
+
} else {
|
|
732
|
+
this._toggleDropdown();
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
_toggleDropdown() {
|
|
737
|
+
const dd = this.shadowRoot.querySelector('.dropdown');
|
|
738
|
+
if (dd.classList.contains('open')) {
|
|
739
|
+
this._closeDropdown();
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
this._renderIssuers();
|
|
743
|
+
|
|
744
|
+
const btn = this.shadowRoot.querySelector('.auth-btn');
|
|
745
|
+
const rect = btn.getBoundingClientRect();
|
|
746
|
+
dd.style.top = (rect.bottom + 4) + 'px';
|
|
747
|
+
dd.classList.add('open');
|
|
748
|
+
requestAnimationFrame(() => {
|
|
749
|
+
const dw = dd.offsetWidth;
|
|
750
|
+
const clampedLeft = Math.max(4, Math.min(rect.right - dw, window.innerWidth - dw - 4));
|
|
751
|
+
dd.style.left = clampedLeft + 'px';
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
const input = this.shadowRoot.querySelector('.issuer-input');
|
|
755
|
+
input.value = this._issuers[0] || '';
|
|
756
|
+
input.focus();
|
|
757
|
+
|
|
758
|
+
const close = (e) => {
|
|
759
|
+
if (!dd.contains(e.composedPath()[0]) && e.composedPath()[0] !== btn) {
|
|
760
|
+
this._closeDropdown();
|
|
761
|
+
document.removeEventListener('click', close, true);
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
setTimeout(() => document.addEventListener('click', close, true), 0);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
_closeDropdown() {
|
|
768
|
+
const dd = this.shadowRoot.querySelector('.dropdown');
|
|
769
|
+
if (dd) dd.classList.remove('open');
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/** Insert (or update) a tiny hint at the top of the dropdown that
|
|
773
|
+
* names the default issuer auto-login is using and prompts the user
|
|
774
|
+
* to pick another to switch. Idempotent — calling twice updates
|
|
775
|
+
* the hint text instead of stacking it. */
|
|
776
|
+
_showSwitchHint(defaultIssuer) {
|
|
777
|
+
const dd = this.shadowRoot.querySelector('.dropdown');
|
|
778
|
+
if (!dd) return;
|
|
779
|
+
let hint = dd.querySelector('.switch-hint');
|
|
780
|
+
if (!hint) {
|
|
781
|
+
hint = document.createElement('div');
|
|
782
|
+
hint.className = 'switch-hint';
|
|
783
|
+
dd.insertBefore(hint, dd.firstChild);
|
|
784
|
+
}
|
|
785
|
+
const short = defaultIssuer.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
786
|
+
hint.textContent = `Signing in as ${short} — pick another to switch`;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
_hideSwitchHint() {
|
|
790
|
+
const hint = this.shadowRoot.querySelector('.switch-hint');
|
|
791
|
+
if (hint) hint.remove();
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
_renderIssuers() {
|
|
795
|
+
const list = this.shadowRoot.querySelector('.issuer-list');
|
|
796
|
+
if (!list) return;
|
|
797
|
+
list.innerHTML = '';
|
|
798
|
+
this._issuers.forEach(issuer => {
|
|
799
|
+
const btn = document.createElement('button');
|
|
800
|
+
btn.className = 'issuer-item';
|
|
801
|
+
btn.textContent = issuer.replace(/^https?:\/\//, '');
|
|
802
|
+
btn.title = issuer;
|
|
803
|
+
btn.onclick = () => {
|
|
804
|
+
this._closeDropdown();
|
|
805
|
+
const url = issuer.endsWith('/') ? issuer : issuer + '/';
|
|
806
|
+
this.login(url);
|
|
807
|
+
};
|
|
808
|
+
list.appendChild(btn);
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
async _loginCustom() {
|
|
813
|
+
const input = this.shadowRoot.querySelector('.issuer-input');
|
|
814
|
+
const val = input.value.trim();
|
|
815
|
+
if (!val) return;
|
|
816
|
+
const issuer = val.endsWith('/') ? val : val + '/';
|
|
817
|
+
this._closeDropdown();
|
|
818
|
+
await this.login(issuer);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/** Show a transient message in the status span (overwritten by _updateUI). */
|
|
822
|
+
_setStatusMessage(msg, isErr) {
|
|
823
|
+
const status = this.shadowRoot && this.shadowRoot.querySelector('.auth-status');
|
|
824
|
+
if (!status) return;
|
|
825
|
+
status.textContent = msg;
|
|
826
|
+
status.className = 'auth-status' + (isErr ? ' auth-error' : '');
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
_updateUI() {
|
|
830
|
+
const status = this.shadowRoot.querySelector('.auth-status');
|
|
831
|
+
const btn = this.shadowRoot.querySelector('.auth-btn');
|
|
832
|
+
if (!status || !btn) return;
|
|
833
|
+
|
|
834
|
+
const session = this._mode === 'popup'
|
|
835
|
+
? this._sideSession()
|
|
836
|
+
: this._auth.getFirstLoggedIn();
|
|
837
|
+
// The WebID is surfaced only as the button's hover title, never as
|
|
838
|
+
// visible page text.
|
|
839
|
+
status.textContent = '';
|
|
840
|
+
if (session && session.info && session.info.isLoggedIn) {
|
|
841
|
+
status.className = 'auth-status logged-in';
|
|
842
|
+
btn.textContent = 'Log out';
|
|
843
|
+
btn.className = 'sol-btn sol-btn-sm auth-btn logged-in';
|
|
844
|
+
btn.title = session.info.webId || '';
|
|
845
|
+
} else {
|
|
846
|
+
status.className = 'auth-status';
|
|
847
|
+
btn.textContent = 'Log in';
|
|
848
|
+
btn.className = 'sol-btn sol-btn-sm sol-btn-primary auth-btn';
|
|
849
|
+
btn.title = '';
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
define('sol-login', SolLogin);
|
|
855
|
+
export { SolLogin, AuthManager };
|
|
856
|
+
export default SolLogin;
|