@hotosm/hanko-auth 0.4.8 → 0.4.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotosm/hanko-auth",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "Web component for HOTOSM SSO authentication with Hanko and OSM integration",
5
5
  "type": "module",
6
6
  "main": "dist/hanko-auth.umd.js",
@@ -21,7 +21,8 @@
21
21
  "dev": "vite",
22
22
  "build": "vite build",
23
23
  "watch": "vite build --watch",
24
- "preview": "vite preview"
24
+ "preview": "vite preview",
25
+ "prepublishOnly": "pnpm build"
25
26
  },
26
27
  "keywords": [
27
28
  "hotosm",
@@ -25,8 +25,10 @@ export const styles = css`
25
25
  display: flex;
26
26
  flex-direction: column;
27
27
  align-items: center;
28
+ width: 100%;
28
29
  gap: var(--hot-spacing-small);
29
30
  padding: var(--hot-spacing-large);
31
+ box-sizing: border-box;
30
32
  }
31
33
 
32
34
  .spinner {
@@ -147,15 +149,19 @@ export const styles = css`
147
149
  font-weight: var(--hot-font-weight-bold);
148
150
  color: var(--hot-color-gray-600);
149
151
  overflow: hidden;
152
+ flex-shrink: 0;
150
153
  }
151
154
 
152
155
  .profile-info {
153
- padding: var(--hot-spacing-x-small) var(--hot-spacing-medium);
156
+ min-width: 0;
154
157
  }
155
158
 
156
159
  .profile-email {
157
160
  font-size: var(--hot-font-size-small);
158
161
  font-weight: var(--hot-font-weight-bold);
162
+ overflow: hidden;
163
+ text-overflow: ellipsis;
164
+ white-space: nowrap;
159
165
  }
160
166
 
161
167
  .osm-section {
@@ -260,6 +266,7 @@ export const styles = css`
260
266
  overflow: hidden;
261
267
  font-weight: var(--hot-font-weight-semibold);
262
268
  color: white;
269
+ flex-shrink: 0;
263
270
  }
264
271
 
265
272
  .login-link {
package/src/hanko-auth.ts CHANGED
@@ -1,15 +1,6 @@
1
- /**
2
- * @hotosm/hanko-auth Web Component (Lit Version)
3
- *
4
- * Smart authentication component that handles:
5
- * - Hanko SSO (Google, GitHub, Email)
6
- * - Optional OSM connection
7
- * - Session management
8
- * - Event dispatching
9
- * - URL fallback chain for production builds
10
- */
11
-
12
- import { LitElement, html, css } from "lit";
1
+ /** HOTOSM Hanko auth web component. */
2
+
3
+ import { LitElement, html } from "lit";
13
4
  import { customElement, property, state } from "lit/decorators.js";
14
5
  import { keyed } from "lit/directives/keyed.js";
15
6
  import { register } from "@teamhanko/hanko-elements";
@@ -85,6 +76,7 @@ interface UserState {
85
76
  email: string | null;
86
77
  username: string | null;
87
78
  emailVerified: boolean;
79
+ avatarUrl?: string;
88
80
  }
89
81
 
90
82
  interface OSMData {
@@ -202,12 +194,26 @@ export class HankoAuth extends LitElement {
202
194
  }
203
195
 
204
196
  // Private fields
205
- private _trailingSlashCache: Record<string, boolean> = {};
206
197
  private _debugMode = false;
207
198
  private _lastSessionId: string | null = null;
208
199
  private _hanko: any = null;
209
200
  private _isPrimary = false; // Is this the primary instance?
210
201
 
202
+ constructor() {
203
+ super();
204
+ try {
205
+ const cached = localStorage.getItem("hotosm-auth-user");
206
+ if (cached) {
207
+ const cachedUser: UserState = JSON.parse(cached);
208
+ this.user = cachedUser;
209
+ this.loading = false;
210
+ if (cachedUser.avatarUrl) {
211
+ this.profilePictureUrl = cachedUser.avatarUrl;
212
+ }
213
+ }
214
+ } catch {}
215
+ }
216
+
211
217
  // Get computed hankoUrl (priority: attribute > meta tag > window.HANKO_URL > origin)
212
218
  get hankoUrl(): string {
213
219
  if (this.hankoUrlAttr) {
@@ -218,28 +224,28 @@ export class HankoAuth extends LitElement {
218
224
  if (metaTag) {
219
225
  const content = metaTag.getAttribute("content");
220
226
  if (content) {
221
- this.log("🔍 hanko-url auto-detected from <meta> tag:", content);
227
+ this.log("hanko-url auto-detected from <meta> tag:", content);
222
228
  return content;
223
229
  }
224
230
  }
225
231
 
226
232
  if ((window as any).HANKO_URL) {
227
233
  this.log(
228
- "🔍 hanko-url auto-detected from window.HANKO_URL:",
234
+ "hanko-url auto-detected from window.HANKO_URL:",
229
235
  (window as any).HANKO_URL,
230
236
  );
231
237
  return (window as any).HANKO_URL;
232
238
  }
233
239
 
234
240
  const origin = window.location.origin;
235
- this.log("🔍 hanko-url auto-detected from window.location.origin:", origin);
241
+ this.log("hanko-url auto-detected from window.location.origin:", origin);
236
242
  return origin;
237
243
  }
238
244
 
239
245
  connectedCallback() {
240
246
  super.connectedCallback();
241
247
  this._debugMode = this._checkDebugMode();
242
- this.log("🔌 hanko-auth connectedCallback called");
248
+ this.log("hanko-auth connectedCallback called");
243
249
 
244
250
  // Inject Hot styles early, before any Hanko elements render
245
251
  this.injectHotStyles();
@@ -257,7 +263,7 @@ export class HankoAuth extends LitElement {
257
263
 
258
264
  // Use firstUpdated instead of connectedCallback to ensure React props are set
259
265
  firstUpdated() {
260
- this.log("🔌 hanko-auth firstUpdated called");
266
+ this.log("hanko-auth firstUpdated called");
261
267
  this.log(" hankoUrl:", this.hankoUrl);
262
268
  this.log(" basePath:", this.basePath);
263
269
 
@@ -359,7 +365,7 @@ export class HankoAuth extends LitElement {
359
365
  if (!this.showProfile && !this.user) {
360
366
  // Window focused, we're in header mode, and no user is logged in
361
367
  // Re-check session in case user logged in
362
- this.log("🎯 Window focused, re-checking session...");
368
+ this.log("Window focused, re-checking session...");
363
369
  this.checkSession();
364
370
  }
365
371
  };
@@ -371,7 +377,7 @@ export class HankoAuth extends LitElement {
371
377
  const customEvent = event as CustomEvent;
372
378
  if (!this.showProfile && !this.user && customEvent.detail?.user) {
373
379
  // Another component (e.g., login page) logged in
374
- this.log("🔔 External login detected, updating user state...");
380
+ this.log("External login detected, updating user state...");
375
381
  this.user = customEvent.detail.user;
376
382
  this._broadcastState();
377
383
  // Also re-check OSM connection (only if required)
@@ -410,10 +416,6 @@ export class HankoAuth extends LitElement {
410
416
  return langTranslations[key] || translations.en[key] || key;
411
417
  }
412
418
 
413
- private warn(...args: any[]) {
414
- console.warn(...args);
415
- }
416
-
417
419
  private logError(...args: any[]) {
418
420
  console.error(...args);
419
421
  }
@@ -421,24 +423,16 @@ export class HankoAuth extends LitElement {
421
423
  private getBasePath(): string {
422
424
  // Use basePath property directly (works with both attribute and React props)
423
425
  if (this.basePath) {
424
- this.log("🔍 getBasePath() using basePath:", this.basePath);
426
+ this.log("getBasePath() using basePath:", this.basePath);
425
427
  return this.basePath;
426
428
  }
427
429
 
428
430
  // For single-page apps (like Portal), default to empty base path
429
431
  // The authPath already contains the full API path
430
- this.log("🔍 getBasePath() using default: empty string");
432
+ this.log("getBasePath() using default: empty string");
431
433
  return "";
432
434
  }
433
435
 
434
- private addTrailingSlash(path: string, basePath: string): string {
435
- const needsSlash = this._trailingSlashCache[basePath];
436
- if (needsSlash !== undefined && needsSlash && !path.endsWith("/")) {
437
- return path + "/";
438
- }
439
- return path;
440
- }
441
-
442
436
  // styles injected to ensure global availability
443
437
  private injectHotStyles() {
444
438
  const stylesheets = [
@@ -466,7 +460,7 @@ export class HankoAuth extends LitElement {
466
460
  private async init() {
467
461
  // Only primary instance should initialize
468
462
  if (!this._isPrimary) {
469
- this.log("⏭️ Not primary, skipping init...");
463
+ this.log("Not primary, skipping init...");
470
464
  return;
471
465
  }
472
466
 
@@ -505,12 +499,12 @@ export class HankoAuth extends LitElement {
505
499
 
506
500
  // Set up session lifecycle event listeners (these persist across the component lifecycle)
507
501
  this._hanko.onSessionExpired(() => {
508
- this.log("🕒 Hanko session expired event received");
502
+ this.log("Hanko session expired event received");
509
503
  this.handleSessionExpired();
510
504
  });
511
505
 
512
506
  this._hanko.onUserLoggedOut(() => {
513
- this.log("🚪 Hanko user logged out event received");
507
+ this.log("Hanko user logged out event received");
514
508
  this.handleUserLoggedOut();
515
509
  });
516
510
 
@@ -537,7 +531,7 @@ export class HankoAuth extends LitElement {
537
531
  }
538
532
 
539
533
  private async checkSession() {
540
- this.log("🔍 Checking for existing Hanko session...");
534
+ this.log("Checking for existing Hanko session...");
541
535
 
542
536
  if (!this._hanko) {
543
537
  this.log("Hanko instance not initialized yet");
@@ -569,6 +563,13 @@ export class HankoAuth extends LitElement {
569
563
  this.log(
570
564
  "Session validation returned is_valid:false - no valid session",
571
565
  );
566
+ if (this.user) {
567
+ this.user = null;
568
+ this.profilePictureUrl = "";
569
+ this.dispatchEvent(
570
+ new CustomEvent("logout", { bubbles: true, composed: true }),
571
+ );
572
+ }
572
573
  return;
573
574
  }
574
575
 
@@ -657,6 +658,10 @@ export class HankoAuth extends LitElement {
657
658
  // Redirect to onboarding in progress, don't proceed
658
659
  return;
659
660
  }
661
+ await this.fetchProfileDisplayName();
662
+ if (this.user && this.profilePictureUrl) {
663
+ this.user = { ...this.user, avatarUrl: this.profilePictureUrl };
664
+ }
660
665
 
661
666
  this.dispatchEvent(
662
667
  new CustomEvent("hanko-login", {
@@ -677,8 +682,6 @@ export class HankoAuth extends LitElement {
677
682
  if (this.osmRequired) {
678
683
  await this.checkOSMConnection();
679
684
  }
680
- // Fetch profile display name
681
- await this.fetchProfileDisplayName();
682
685
  if (this.osmRequired && this.autoConnect && !this.osmConnected) {
683
686
  this.log("Auto-connecting to OSM (from existing session)...");
684
687
  this.handleOSMConnect();
@@ -879,12 +882,9 @@ export class HankoAuth extends LitElement {
879
882
  this.log("Display name set to:", this.profileDisplayName);
880
883
  }
881
884
 
882
- // picture_url is always set by the backend (Gravatar fallback); osm_avatar_url as secondary
883
- const picUrl = profile.picture_url || profile.osm_avatar_url;
884
- if (picUrl) {
885
- this.profilePictureUrl = picUrl;
886
- this.log("Profile picture set to:", this.profilePictureUrl);
887
- }
885
+ const picUrl = profile.osm_avatar_url || profile.picture_url;
886
+ this.profilePictureUrl = picUrl || "";
887
+ this.log("Profile picture set to:", this.profilePictureUrl);
888
888
 
889
889
  // Set language from user profile if available
890
890
  if (profile.language) {
@@ -909,7 +909,7 @@ export class HankoAuth extends LitElement {
909
909
  this.user === null &&
910
910
  this.showProfile
911
911
  ) {
912
- this.log("🔄 User logged out, re-attaching event listeners...");
912
+ this.log("User logged out, re-attaching event listeners...");
913
913
  this._currentHankoAuthElement = null;
914
914
  this.setupEventListeners();
915
915
  }
@@ -928,7 +928,7 @@ export class HankoAuth extends LitElement {
928
928
 
929
929
  if (hankoAuth) {
930
930
  this._currentHankoAuthElement = hankoAuth;
931
- this.log("🎯 Attaching event listeners to hanko-auth element");
931
+ this.log("Attaching event listeners to hanko-auth element");
932
932
 
933
933
  hankoAuth.addEventListener("onSessionCreated", (e: any) => {
934
934
  this.log(`Hanko event: onSessionCreated`, e.detail);
@@ -1058,6 +1058,10 @@ export class HankoAuth extends LitElement {
1058
1058
  }
1059
1059
 
1060
1060
  this.log("User state updated:", this.user);
1061
+ await this.fetchProfileDisplayName();
1062
+ if (this.user && this.profilePictureUrl) {
1063
+ this.user = { ...this.user, avatarUrl: this.profilePictureUrl };
1064
+ }
1061
1065
 
1062
1066
  // Broadcast state changes to other instances
1063
1067
  if (this._isPrimary) {
@@ -1076,8 +1080,6 @@ export class HankoAuth extends LitElement {
1076
1080
  if (this.osmRequired) {
1077
1081
  await this.checkOSMConnection();
1078
1082
  }
1079
- // Fetch profile display name (only works with login.hotosm.org backend)
1080
- await this.fetchProfileDisplayName();
1081
1083
 
1082
1084
  // Auto-connect to OSM if required and auto-connect is enabled
1083
1085
  if (this.osmRequired && this.autoConnect && !this.osmConnected) {
@@ -1366,47 +1368,9 @@ export class HankoAuth extends LitElement {
1366
1368
  // Close dropdown after selection
1367
1369
  this.closeDropdown();
1368
1370
  }
1369
- private oldHandleDropdownSelect(event: CustomEvent) {
1370
- const selectedValue = event.detail.item.value;
1371
- this.log("🎯 Dropdown item selected:", selectedValue);
1372
-
1373
- if (selectedValue === "profile") {
1374
- // Profile page: standalone apps have their own, others use central login service
1375
- // loginUrl already includes /app, hankoUrl doesn't
1376
- const returnTo = this.redirectAfterLogin || window.location.origin;
1377
- const profileUrl = this.loginUrl
1378
- ? `${this.loginUrl}/profile`
1379
- : `${this.hankoUrl}/app/profile`;
1380
- window.location.href = `${profileUrl}?return_to=${encodeURIComponent(returnTo)}`;
1381
- } else if (selectedValue === "connect-osm") {
1382
- // Smart return_to: if already on a login page, redirect to home instead
1383
- const currentPath = window.location.pathname;
1384
- const isOnLoginPage = currentPath.includes("/app");
1385
- const returnTo = isOnLoginPage
1386
- ? window.location.origin
1387
- : window.location.href;
1388
-
1389
- // Use the getter which handles all fallbacks correctly
1390
- const baseUrl = this.hankoUrl;
1391
- window.location.href = `${baseUrl}/app?return_to=${encodeURIComponent(
1392
- returnTo,
1393
- )}&osm_required=true`;
1394
- } else if (selectedValue === "logout") {
1395
- this.handleLogout();
1396
- }
1397
- }
1398
-
1399
- private handleSkipOSM() {
1400
- this.dispatchEvent(new CustomEvent("osm-skipped"));
1401
- this.dispatchEvent(new CustomEvent("auth-complete"));
1402
- if (this.redirectAfterLogin) {
1403
- window.location.href = this.redirectAfterLogin;
1404
- }
1405
- }
1406
-
1407
1371
  render() {
1408
1372
  this.log(
1409
- "🎨 RENDER - showProfile:",
1373
+ "RENDER - showProfile:",
1410
1374
  this.showProfile,
1411
1375
  "user:",
1412
1376
  !!this.user,
@@ -1444,8 +1408,6 @@ export class HankoAuth extends LitElement {
1444
1408
  const initial = displayName ? displayName[0].toUpperCase() : "U";
1445
1409
 
1446
1410
  if (this.showProfile) {
1447
- // Show full profile view
1448
- // TODO check use cases
1449
1411
  return html`
1450
1412
  <div class="container">
1451
1413
  <div class="profile">
@@ -1652,13 +1614,12 @@ export class HankoAuth extends LitElement {
1652
1614
  `;
1653
1615
  }
1654
1616
  } else {
1655
- // Not logged in
1617
+ // Not logged in.
1656
1618
  if (this.showProfile) {
1657
- // On login page - show full Hanko auth form
1658
- // Don't render until Hanko is registered to prevent 404 errors
1619
+ // Login page mode: render full Hanko form after registration is ready.
1659
1620
  if (!this.hankoReady) {
1660
1621
  this.log(
1661
- "Waiting for Hanko registration before rendering form...",
1622
+ "Waiting for Hanko registration before rendering form...",
1662
1623
  );
1663
1624
  return html`<span class="loading-placeholder"
1664
1625
  ><span class="loading-placeholder-text">${this.t("logIn")}</span
@@ -1697,9 +1658,7 @@ export class HankoAuth extends LitElement {
1697
1658
  </div>
1698
1659
  `;
1699
1660
  } else {
1700
- // In header - show login link
1701
- // Use redirectAfterLogin if set, otherwise use current URL
1702
- // Smart return_to: if already on a login page, redirect to home instead
1661
+ // Header mode: render login link with a safe return_to target.
1703
1662
  const currentPath = window.location.pathname;
1704
1663
  const isOnLoginPage = currentPath.includes("/app");
1705
1664
  const returnTo =
@@ -1710,34 +1669,15 @@ export class HankoAuth extends LitElement {
1710
1669
  const autoConnectParam =
1711
1670
  urlParams.get("auto_connect") === "true" ? "&auto_connect=true" : "";
1712
1671
 
1713
- // Use the getter which handles all fallbacks correctly
1714
1672
  const baseUrl = this.hankoUrl;
1715
- this.log("🔗 Login URL base:", baseUrl);
1673
+ this.log("Login URL base:", baseUrl);
1716
1674
 
1717
- // Use custom loginUrl if provided (for standalone mode), otherwise use ${hankoUrl}/app
1675
+ // Use custom loginUrl when provided; fallback to {hankoUrl}/app.
1718
1676
  const loginBase = this.loginUrl || `${baseUrl}/app`;
1719
1677
  const loginUrl = `${loginBase}?return_to=${encodeURIComponent(
1720
1678
  returnTo,
1721
1679
  )}${this.osmRequired ? "&osm_required=true" : ""}${autoConnectParam}&lang=${this.lang}`;
1722
1680
 
1723
- /* if (this.display === "bar") {
1724
- return html`<a
1725
- class="bar-trigger login-link ${this.buttonVariant} ${this.buttonColor}"
1726
- href="${loginUrl}"
1727
- @click=${(e: Event) => {
1728
- e.preventDefault();
1729
- window.location.href = loginUrl;
1730
- }}
1731
- >
1732
- <span class="bar-email">${this.t("logIn")}</span>
1733
- <img
1734
- src="${chevronDownIcon}"
1735
- class="bar-chevron"
1736
- alt=""
1737
- />
1738
- </a>`;
1739
- } */
1740
-
1741
1681
  return html`<a
1742
1682
  class="login-link ${this.buttonVariant} ${this.buttonColor}"
1743
1683
  href="${loginUrl}"