@hotosm/hanko-auth 0.4.7 โ 0.4.9
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 +20 -0
- package/dist/hanko-auth.esm.js +1590 -1528
- package/dist/hanko-auth.iife.js +92 -42
- package/dist/hanko-auth.umd.js +94 -44
- package/package.json +3 -2
- package/src/hanko-auth.styles.ts +32 -3
- package/src/hanko-auth.ts +170 -120
package/src/hanko-auth.ts
CHANGED
|
@@ -34,8 +34,7 @@ import chevronUpIcon from "../assets/chevron-up.svg";
|
|
|
34
34
|
let hankoRegistered = false;
|
|
35
35
|
let hankoRegistrationPromise: Promise<void> | null = null;
|
|
36
36
|
|
|
37
|
-
// Pre-register translations
|
|
38
|
-
// This will be called again with hankoUrl when component initializes
|
|
37
|
+
// Pre-register translations
|
|
39
38
|
async function ensureHankoRegistered(hankoUrl: string): Promise<void> {
|
|
40
39
|
if (hankoRegistered) return;
|
|
41
40
|
if (hankoRegistrationPromise) return hankoRegistrationPromise;
|
|
@@ -72,6 +71,7 @@ const sharedAuth = {
|
|
|
72
71
|
initialized: false,
|
|
73
72
|
instances: new Set<any>(),
|
|
74
73
|
profileDisplayName: "", // Shared profile display name
|
|
74
|
+
profilePictureUrl: "", // Shared profile picture URL
|
|
75
75
|
hankoReady: false, // used for translations
|
|
76
76
|
};
|
|
77
77
|
|
|
@@ -85,6 +85,7 @@ interface UserState {
|
|
|
85
85
|
email: string | null;
|
|
86
86
|
username: string | null;
|
|
87
87
|
emailVerified: boolean;
|
|
88
|
+
avatarUrl?: string;
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
interface OSMData {
|
|
@@ -112,26 +113,20 @@ export class HankoAuth extends LitElement {
|
|
|
112
113
|
redirectAfterLogout = "";
|
|
113
114
|
@property({ type: String, attribute: "display-name" })
|
|
114
115
|
displayNameAttr = "";
|
|
115
|
-
// URL to check if user has app mapping (for cross-app auth scenarios)
|
|
116
116
|
@property({ type: String, attribute: "mapping-check-url" }) mappingCheckUrl =
|
|
117
117
|
"";
|
|
118
|
-
// App identifier for onboarding redirect
|
|
119
118
|
@property({ type: String, attribute: "app-id" }) appId = "";
|
|
120
119
|
// Custom login page URL (for standalone mode - overrides ${hankoUrl}/app)
|
|
121
120
|
@property({ type: String, attribute: "login-url" }) loginUrl = "";
|
|
122
|
-
// Language code (en, es, fr, pt, etc.)
|
|
123
121
|
@property({ type: String, reflect: true }) lang = "en";
|
|
124
|
-
// Button variant (filled, outline, plain)
|
|
125
122
|
@property({ type: String, attribute: "button-variant" }) buttonVariant:
|
|
126
123
|
| "filled"
|
|
127
124
|
| "outline"
|
|
128
125
|
| "plain" = "plain";
|
|
129
|
-
// Button color (primary, neutral, danger)
|
|
130
126
|
@property({ type: String, attribute: "button-color" }) buttonColor:
|
|
131
127
|
| "primary"
|
|
132
128
|
| "neutral"
|
|
133
129
|
| "danger" = "primary";
|
|
134
|
-
// Display mode: "default" (compact avatar button) or "bar" (full-width bar with avatar + email + chevron)
|
|
135
130
|
@property({ type: String, reflect: true }) display: "default" | "bar" =
|
|
136
131
|
"default";
|
|
137
132
|
|
|
@@ -144,6 +139,7 @@ export class HankoAuth extends LitElement {
|
|
|
144
139
|
@state() private error: string | null = null;
|
|
145
140
|
@state() private hankoReady = false; // Tracks when Hanko registration is complete
|
|
146
141
|
@state() private profileDisplayName: string = "";
|
|
142
|
+
@state() private profilePictureUrl: string = "";
|
|
147
143
|
@state() private hasAppMapping = false; // True if user has mapping in the app
|
|
148
144
|
@state() private userProfileLanguage: string | null = null; // Language from user profile
|
|
149
145
|
// dropdown
|
|
@@ -213,6 +209,21 @@ export class HankoAuth extends LitElement {
|
|
|
213
209
|
private _hanko: any = null;
|
|
214
210
|
private _isPrimary = false; // Is this the primary instance?
|
|
215
211
|
|
|
212
|
+
constructor() {
|
|
213
|
+
super();
|
|
214
|
+
try {
|
|
215
|
+
const cached = localStorage.getItem("hotosm-auth-user");
|
|
216
|
+
if (cached) {
|
|
217
|
+
const cachedUser: UserState = JSON.parse(cached);
|
|
218
|
+
this.user = cachedUser;
|
|
219
|
+
this.loading = false;
|
|
220
|
+
if (cachedUser.avatarUrl) {
|
|
221
|
+
this.profilePictureUrl = cachedUser.avatarUrl;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} catch {}
|
|
225
|
+
}
|
|
226
|
+
|
|
216
227
|
// Get computed hankoUrl (priority: attribute > meta tag > window.HANKO_URL > origin)
|
|
217
228
|
get hankoUrl(): string {
|
|
218
229
|
if (this.hankoUrlAttr) {
|
|
@@ -268,12 +279,12 @@ export class HankoAuth extends LitElement {
|
|
|
268
279
|
|
|
269
280
|
// If already initialized or being initialized by another instance, sync state and skip init
|
|
270
281
|
if (sharedAuth.initialized || sharedAuth.primary) {
|
|
271
|
-
this.log("
|
|
282
|
+
this.log("Using shared state from primary instance");
|
|
272
283
|
this._syncFromShared();
|
|
273
284
|
this._isPrimary = false;
|
|
274
285
|
} else {
|
|
275
286
|
// This is the first/primary instance - claim it immediately to prevent race conditions
|
|
276
|
-
this.log("
|
|
287
|
+
this.log("This is the primary instance");
|
|
277
288
|
this._isPrimary = true;
|
|
278
289
|
sharedAuth.primary = this;
|
|
279
290
|
sharedAuth.initialized = true; // Mark as initialized immediately to prevent other instances from also initializing
|
|
@@ -298,7 +309,7 @@ export class HankoAuth extends LitElement {
|
|
|
298
309
|
if (this._isPrimary && sharedAuth.instances.size > 0) {
|
|
299
310
|
const newPrimary = sharedAuth.instances.values().next().value;
|
|
300
311
|
if (newPrimary) {
|
|
301
|
-
this.log("
|
|
312
|
+
this.log("Promoting new primary instance");
|
|
302
313
|
newPrimary._isPrimary = true;
|
|
303
314
|
sharedAuth.primary = newPrimary;
|
|
304
315
|
}
|
|
@@ -321,6 +332,8 @@ export class HankoAuth extends LitElement {
|
|
|
321
332
|
if (this._hanko !== sharedAuth.hanko) this._hanko = sharedAuth.hanko;
|
|
322
333
|
if (this.profileDisplayName !== sharedAuth.profileDisplayName)
|
|
323
334
|
this.profileDisplayName = sharedAuth.profileDisplayName;
|
|
335
|
+
if (this.profilePictureUrl !== sharedAuth.profilePictureUrl)
|
|
336
|
+
this.profilePictureUrl = sharedAuth.profilePictureUrl;
|
|
324
337
|
if (this.hankoReady !== sharedAuth.hankoReady)
|
|
325
338
|
this.hankoReady = sharedAuth.hankoReady;
|
|
326
339
|
}
|
|
@@ -332,6 +345,7 @@ export class HankoAuth extends LitElement {
|
|
|
332
345
|
sharedAuth.osmData = this.osmData;
|
|
333
346
|
sharedAuth.loading = this.loading;
|
|
334
347
|
sharedAuth.profileDisplayName = this.profileDisplayName;
|
|
348
|
+
sharedAuth.profilePictureUrl = this.profilePictureUrl;
|
|
335
349
|
sharedAuth.hankoReady = this.hankoReady;
|
|
336
350
|
|
|
337
351
|
// Sync to all other instances
|
|
@@ -349,7 +363,7 @@ export class HankoAuth extends LitElement {
|
|
|
349
363
|
if (!document.hidden && !this.showProfile && !this.user) {
|
|
350
364
|
// Page became visible, we're in header mode, and no user is logged in
|
|
351
365
|
// Re-check session in case user logged in elsewhere
|
|
352
|
-
this.log("
|
|
366
|
+
this.log("Page visible, re-checking session...");
|
|
353
367
|
this.checkSession();
|
|
354
368
|
}
|
|
355
369
|
};
|
|
@@ -402,13 +416,8 @@ export class HankoAuth extends LitElement {
|
|
|
402
416
|
}
|
|
403
417
|
}
|
|
404
418
|
|
|
405
|
-
|
|
406
|
-
* Get translated string for the current language
|
|
407
|
-
* Falls back to English if translation not found
|
|
408
|
-
* When user is logged in, uses their profile language instead of the lang prop
|
|
409
|
-
*/
|
|
419
|
+
/* Translations */
|
|
410
420
|
private t(key: keyof typeof translations.en): string {
|
|
411
|
-
// When user is logged in, use their profile language
|
|
412
421
|
const effectiveLang =
|
|
413
422
|
this.user && this.userProfileLanguage
|
|
414
423
|
? this.userProfileLanguage
|
|
@@ -547,12 +556,12 @@ export class HankoAuth extends LitElement {
|
|
|
547
556
|
this.log("๐ Checking for existing Hanko session...");
|
|
548
557
|
|
|
549
558
|
if (!this._hanko) {
|
|
550
|
-
this.log("
|
|
559
|
+
this.log("Hanko instance not initialized yet");
|
|
551
560
|
return;
|
|
552
561
|
}
|
|
553
562
|
|
|
554
563
|
try {
|
|
555
|
-
this.log("
|
|
564
|
+
this.log("Checking session validity via cookie...");
|
|
556
565
|
|
|
557
566
|
// First, try to validate the session cookie directly with Hanko
|
|
558
567
|
// This works across subdomains because the cookie has domain: .hotosm.test
|
|
@@ -574,13 +583,13 @@ export class HankoAuth extends LitElement {
|
|
|
574
583
|
// Check if session is actually valid (endpoint returns 200 with is_valid:false when no session)
|
|
575
584
|
if (sessionData.is_valid === false) {
|
|
576
585
|
this.log(
|
|
577
|
-
"
|
|
586
|
+
"Session validation returned is_valid:false - no valid session",
|
|
578
587
|
);
|
|
579
588
|
return;
|
|
580
589
|
}
|
|
581
590
|
|
|
582
|
-
this.log("
|
|
583
|
-
this.log("
|
|
591
|
+
this.log("Valid Hanko session found via cookie");
|
|
592
|
+
this.log("Session data:", sessionData);
|
|
584
593
|
|
|
585
594
|
// Now get the full user data from the login backend /me endpoint
|
|
586
595
|
// This endpoint validates the JWT and returns complete user info
|
|
@@ -596,7 +605,7 @@ export class HankoAuth extends LitElement {
|
|
|
596
605
|
let needsSdkFallback = true;
|
|
597
606
|
if (meResponse.ok) {
|
|
598
607
|
const userData = await meResponse.json();
|
|
599
|
-
this.log("
|
|
608
|
+
this.log("User data retrieved from /me:", userData);
|
|
600
609
|
|
|
601
610
|
// Only use /me if it has email (login.hotosm.org has it, Hanko vanilla doesn't)
|
|
602
611
|
if (userData.email) {
|
|
@@ -609,12 +618,12 @@ export class HankoAuth extends LitElement {
|
|
|
609
618
|
};
|
|
610
619
|
needsSdkFallback = false;
|
|
611
620
|
} else {
|
|
612
|
-
this.log("
|
|
621
|
+
this.log("/me has no email, will use SDK fallback");
|
|
613
622
|
}
|
|
614
623
|
}
|
|
615
624
|
|
|
616
625
|
if (needsSdkFallback) {
|
|
617
|
-
this.log("
|
|
626
|
+
this.log("Using SDK to get user with email");
|
|
618
627
|
// Fallback to SDK method which has email
|
|
619
628
|
const user = await this._hanko.user.getCurrent();
|
|
620
629
|
this.user = {
|
|
@@ -625,7 +634,7 @@ export class HankoAuth extends LitElement {
|
|
|
625
634
|
};
|
|
626
635
|
}
|
|
627
636
|
} catch (userError) {
|
|
628
|
-
this.log("
|
|
637
|
+
this.log("Failed to get user data:", userError);
|
|
629
638
|
// Last resort: use session data if available
|
|
630
639
|
if (sessionData.user_id) {
|
|
631
640
|
this.user = {
|
|
@@ -650,7 +659,7 @@ export class HankoAuth extends LitElement {
|
|
|
650
659
|
!alreadyVerified
|
|
651
660
|
) {
|
|
652
661
|
this.log(
|
|
653
|
-
"
|
|
662
|
+
"verify-session enabled, redirecting to callback for app verification...",
|
|
654
663
|
);
|
|
655
664
|
sessionStorage.setItem(verifyKey, "true");
|
|
656
665
|
window.location.href = this.redirectAfterLogin;
|
|
@@ -664,6 +673,10 @@ export class HankoAuth extends LitElement {
|
|
|
664
673
|
// Redirect to onboarding in progress, don't proceed
|
|
665
674
|
return;
|
|
666
675
|
}
|
|
676
|
+
await this.fetchProfileDisplayName();
|
|
677
|
+
if (this.user && this.profilePictureUrl) {
|
|
678
|
+
this.user = { ...this.user, avatarUrl: this.profilePictureUrl };
|
|
679
|
+
}
|
|
667
680
|
|
|
668
681
|
this.dispatchEvent(
|
|
669
682
|
new CustomEvent("hanko-login", {
|
|
@@ -684,23 +697,21 @@ export class HankoAuth extends LitElement {
|
|
|
684
697
|
if (this.osmRequired) {
|
|
685
698
|
await this.checkOSMConnection();
|
|
686
699
|
}
|
|
687
|
-
// Fetch profile display name
|
|
688
|
-
await this.fetchProfileDisplayName();
|
|
689
700
|
if (this.osmRequired && this.autoConnect && !this.osmConnected) {
|
|
690
|
-
this.log("
|
|
701
|
+
this.log("Auto-connecting to OSM (from existing session)...");
|
|
691
702
|
this.handleOSMConnect();
|
|
692
703
|
}
|
|
693
704
|
}
|
|
694
705
|
} else {
|
|
695
|
-
this.log("
|
|
706
|
+
this.log("No valid session cookie found - user needs to login");
|
|
696
707
|
}
|
|
697
708
|
} catch (validateError) {
|
|
698
|
-
this.log("
|
|
699
|
-
this.log("
|
|
709
|
+
this.log("Session validation failed:", validateError);
|
|
710
|
+
this.log("No valid session - user needs to login");
|
|
700
711
|
}
|
|
701
712
|
} catch (error) {
|
|
702
|
-
this.log("
|
|
703
|
-
this.log("
|
|
713
|
+
this.log("Session check error:", error);
|
|
714
|
+
this.log("No existing session - user needs to login");
|
|
704
715
|
} finally {
|
|
705
716
|
// Broadcast state changes to other instances
|
|
706
717
|
if (this._isPrimary) {
|
|
@@ -712,12 +723,12 @@ export class HankoAuth extends LitElement {
|
|
|
712
723
|
private async checkOSMConnection() {
|
|
713
724
|
// Skip OSM check if not required
|
|
714
725
|
if (!this.osmRequired) {
|
|
715
|
-
this.log("
|
|
726
|
+
this.log("OSM not required, skipping connection check");
|
|
716
727
|
return;
|
|
717
728
|
}
|
|
718
729
|
|
|
719
730
|
if (this.osmConnected) {
|
|
720
|
-
this.log("
|
|
731
|
+
this.log("Already connected to OSM, skipping check");
|
|
721
732
|
return;
|
|
722
733
|
}
|
|
723
734
|
|
|
@@ -737,23 +748,23 @@ export class HankoAuth extends LitElement {
|
|
|
737
748
|
const statusPath = `${basePath}${authPath}/status`;
|
|
738
749
|
const statusUrl = `${statusPath}`; // Relative URL for proxy
|
|
739
750
|
|
|
740
|
-
this.log("
|
|
751
|
+
this.log("Checking OSM connection at:", statusUrl);
|
|
741
752
|
this.log(" basePath:", basePath);
|
|
742
753
|
this.log(" authPath:", authPath);
|
|
743
|
-
this.log("
|
|
754
|
+
this.log("Current cookies:", document.cookie);
|
|
744
755
|
|
|
745
756
|
const response = await fetch(statusUrl, {
|
|
746
757
|
credentials: "include",
|
|
747
758
|
redirect: "follow",
|
|
748
759
|
});
|
|
749
760
|
|
|
750
|
-
this.log("
|
|
751
|
-
this.log("
|
|
752
|
-
this.log("
|
|
761
|
+
this.log("OSM status response:", response.status);
|
|
762
|
+
this.log("Final URL after redirects:", response.url);
|
|
763
|
+
this.log("Response headers:", [...response.headers.entries()]);
|
|
753
764
|
|
|
754
765
|
if (response.ok) {
|
|
755
766
|
const text = await response.text();
|
|
756
|
-
this.log("
|
|
767
|
+
this.log("OSM raw response:", text.substring(0, 200));
|
|
757
768
|
|
|
758
769
|
let data;
|
|
759
770
|
try {
|
|
@@ -766,10 +777,10 @@ export class HankoAuth extends LitElement {
|
|
|
766
777
|
throw new Error("Invalid JSON response from OSM status endpoint");
|
|
767
778
|
}
|
|
768
779
|
|
|
769
|
-
this.log("
|
|
780
|
+
this.log("OSM status data:", data);
|
|
770
781
|
|
|
771
782
|
if (data.connected) {
|
|
772
|
-
this.log("
|
|
783
|
+
this.log("OSM is connected:", data.osm_username);
|
|
773
784
|
this.osmConnected = true;
|
|
774
785
|
this.osmData = data;
|
|
775
786
|
|
|
@@ -786,7 +797,7 @@ export class HankoAuth extends LitElement {
|
|
|
786
797
|
// The Login page's onboarding flow listens for 'osm-connected' event
|
|
787
798
|
// and handles the redirect to the app's onboarding endpoint
|
|
788
799
|
} else {
|
|
789
|
-
this.log("
|
|
800
|
+
this.log("OSM is NOT connected");
|
|
790
801
|
this.osmConnected = false;
|
|
791
802
|
this.osmData = null;
|
|
792
803
|
}
|
|
@@ -816,12 +827,12 @@ export class HankoAuth extends LitElement {
|
|
|
816
827
|
const onboardingKey = getSessionOnboardingKey(window.location.hostname);
|
|
817
828
|
const onboardingCompleted = sessionStorage.getItem(onboardingKey);
|
|
818
829
|
if (onboardingCompleted) {
|
|
819
|
-
this.log("
|
|
830
|
+
this.log("Onboarding already completed this session, skipping check");
|
|
820
831
|
this.hasAppMapping = true;
|
|
821
832
|
return true;
|
|
822
833
|
}
|
|
823
834
|
|
|
824
|
-
this.log("
|
|
835
|
+
this.log("Checking app mapping at:", this.mappingCheckUrl);
|
|
825
836
|
|
|
826
837
|
try {
|
|
827
838
|
const response = await fetch(this.mappingCheckUrl, {
|
|
@@ -830,12 +841,12 @@ export class HankoAuth extends LitElement {
|
|
|
830
841
|
|
|
831
842
|
if (response.ok) {
|
|
832
843
|
const data = await response.json();
|
|
833
|
-
this.log("
|
|
844
|
+
this.log("Mapping check response:", data);
|
|
834
845
|
|
|
835
846
|
if (data.needs_onboarding) {
|
|
836
847
|
// User has Hanko session but no app mapping - redirect to onboarding
|
|
837
848
|
// Don't set flag here - only set it when onboarding completes
|
|
838
|
-
this.log("
|
|
849
|
+
this.log("User needs onboarding, redirecting...");
|
|
839
850
|
const returnTo = encodeURIComponent(window.location.origin);
|
|
840
851
|
const appParam = this.appId ? `onboarding=${this.appId}` : "";
|
|
841
852
|
window.location.href = `${this.hankoUrl}/app?${appParam}&return_to=${returnTo}`;
|
|
@@ -845,11 +856,11 @@ export class HankoAuth extends LitElement {
|
|
|
845
856
|
// User has mapping - mark onboarding as completed
|
|
846
857
|
sessionStorage.setItem(onboardingKey, "true");
|
|
847
858
|
this.hasAppMapping = true;
|
|
848
|
-
this.log("
|
|
859
|
+
this.log("User has app mapping, onboarding marked complete");
|
|
849
860
|
return true;
|
|
850
861
|
} else if (response.status === 401 || response.status === 403) {
|
|
851
862
|
// Needs onboarding
|
|
852
|
-
this.log("
|
|
863
|
+
this.log("401/403 - User needs onboarding, redirecting...");
|
|
853
864
|
const returnTo = encodeURIComponent(window.location.origin);
|
|
854
865
|
const appParam = this.appId ? `onboarding=${this.appId}` : "";
|
|
855
866
|
window.location.href = `${this.hankoUrl}/app?${appParam}&return_to=${returnTo}`;
|
|
@@ -857,10 +868,10 @@ export class HankoAuth extends LitElement {
|
|
|
857
868
|
}
|
|
858
869
|
|
|
859
870
|
// Other status codes - proceed without blocking
|
|
860
|
-
this.log("
|
|
871
|
+
this.log("Unexpected status from mapping check:", response.status);
|
|
861
872
|
return true;
|
|
862
873
|
} catch (error) {
|
|
863
|
-
this.log("
|
|
874
|
+
this.log("App mapping check failed:", error);
|
|
864
875
|
// Don't block the user, just log the error
|
|
865
876
|
return true;
|
|
866
877
|
}
|
|
@@ -870,7 +881,7 @@ export class HankoAuth extends LitElement {
|
|
|
870
881
|
private async fetchProfileDisplayName() {
|
|
871
882
|
try {
|
|
872
883
|
const profileUrl = `${this.hankoUrl}/api/profile/me`;
|
|
873
|
-
this.log("
|
|
884
|
+
this.log("Fetching profile from:", profileUrl);
|
|
874
885
|
|
|
875
886
|
const response = await fetch(profileUrl, {
|
|
876
887
|
credentials: "include",
|
|
@@ -878,22 +889,29 @@ export class HankoAuth extends LitElement {
|
|
|
878
889
|
|
|
879
890
|
if (response.ok) {
|
|
880
891
|
const profile = await response.json();
|
|
881
|
-
this.log("
|
|
892
|
+
this.log("Profile data:", profile);
|
|
882
893
|
|
|
883
894
|
if (profile.first_name || profile.last_name) {
|
|
884
895
|
this.profileDisplayName =
|
|
885
896
|
`${profile.first_name || ""} ${profile.last_name || ""}`.trim();
|
|
886
|
-
this.log("
|
|
897
|
+
this.log("Display name set to:", this.profileDisplayName);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// picture_url is always set by the backend (Gravatar fallback); osm_avatar_url as secondary
|
|
901
|
+
const picUrl = profile.picture_url || profile.osm_avatar_url;
|
|
902
|
+
if (picUrl) {
|
|
903
|
+
this.profilePictureUrl = picUrl;
|
|
904
|
+
this.log("Profile picture set to:", this.profilePictureUrl);
|
|
887
905
|
}
|
|
888
906
|
|
|
889
907
|
// Set language from user profile if available
|
|
890
908
|
if (profile.language) {
|
|
891
909
|
this.userProfileLanguage = profile.language;
|
|
892
|
-
this.log("
|
|
910
|
+
this.log("Language set from profile:", this.userProfileLanguage);
|
|
893
911
|
}
|
|
894
912
|
}
|
|
895
913
|
} catch (error) {
|
|
896
|
-
this.log("
|
|
914
|
+
this.log("Could not fetch profile:", error);
|
|
897
915
|
}
|
|
898
916
|
}
|
|
899
917
|
|
|
@@ -922,7 +940,7 @@ export class HankoAuth extends LitElement {
|
|
|
922
940
|
|
|
923
941
|
// Skip if already attached to the same element
|
|
924
942
|
if (hankoAuth && hankoAuth === this._currentHankoAuthElement) {
|
|
925
|
-
this.log("
|
|
943
|
+
this.log("Event listeners already attached to this element");
|
|
926
944
|
return;
|
|
927
945
|
}
|
|
928
946
|
|
|
@@ -931,11 +949,11 @@ export class HankoAuth extends LitElement {
|
|
|
931
949
|
this.log("๐ฏ Attaching event listeners to hanko-auth element");
|
|
932
950
|
|
|
933
951
|
hankoAuth.addEventListener("onSessionCreated", (e: any) => {
|
|
934
|
-
this.log(
|
|
952
|
+
this.log(`Hanko event: onSessionCreated`, e.detail);
|
|
935
953
|
|
|
936
954
|
const sessionId = e.detail?.claims?.session_id;
|
|
937
955
|
if (sessionId && this._lastSessionId === sessionId) {
|
|
938
|
-
this.log("
|
|
956
|
+
this.log("Skipping duplicate session event");
|
|
939
957
|
return;
|
|
940
958
|
}
|
|
941
959
|
this._lastSessionId = sessionId;
|
|
@@ -981,7 +999,7 @@ export class HankoAuth extends LitElement {
|
|
|
981
999
|
|
|
982
1000
|
if (meResponse.ok) {
|
|
983
1001
|
const userData = await meResponse.json();
|
|
984
|
-
this.log("
|
|
1002
|
+
this.log("User data retrieved from /me:", userData);
|
|
985
1003
|
|
|
986
1004
|
// Only use /me if it has email (login.hotosm.org has it, Hanko vanilla doesn't)
|
|
987
1005
|
if (userData.email) {
|
|
@@ -994,17 +1012,15 @@ export class HankoAuth extends LitElement {
|
|
|
994
1012
|
};
|
|
995
1013
|
userInfoRetrieved = true;
|
|
996
1014
|
} else {
|
|
997
|
-
this.log("
|
|
1015
|
+
this.log("/me has no email, will try SDK fallback");
|
|
998
1016
|
}
|
|
999
1017
|
} else {
|
|
1000
|
-
this.log(
|
|
1001
|
-
"โ ๏ธ /me endpoint returned non-OK status, will try SDK fallback",
|
|
1002
|
-
);
|
|
1018
|
+
this.log("/me endpoint returned non-OK status, will try SDK fallback");
|
|
1003
1019
|
}
|
|
1004
1020
|
} catch (error) {
|
|
1005
1021
|
// NetworkError or timeout on cross-origin fetch is common with mkcert certs
|
|
1006
1022
|
this.log(
|
|
1007
|
-
"
|
|
1023
|
+
"/me endpoint fetch failed (timeout or cross-origin TLS issue):",
|
|
1008
1024
|
error,
|
|
1009
1025
|
);
|
|
1010
1026
|
}
|
|
@@ -1012,7 +1028,7 @@ export class HankoAuth extends LitElement {
|
|
|
1012
1028
|
// Fallback to SDK method if /me didn't work
|
|
1013
1029
|
if (!userInfoRetrieved) {
|
|
1014
1030
|
try {
|
|
1015
|
-
this.log("
|
|
1031
|
+
this.log("Trying SDK fallback for user info...");
|
|
1016
1032
|
// Add timeout to SDK call in case it hangs
|
|
1017
1033
|
const timeoutPromise = new Promise((_, reject) =>
|
|
1018
1034
|
setTimeout(() => reject(new Error("SDK timeout")), 5000),
|
|
@@ -1028,9 +1044,9 @@ export class HankoAuth extends LitElement {
|
|
|
1028
1044
|
emailVerified: user.email_verified || false,
|
|
1029
1045
|
};
|
|
1030
1046
|
userInfoRetrieved = true;
|
|
1031
|
-
this.log("
|
|
1047
|
+
this.log("User info retrieved via SDK fallback");
|
|
1032
1048
|
} catch (sdkError) {
|
|
1033
|
-
this.log("
|
|
1049
|
+
this.log("SDK fallback failed, trying JWT claims:", sdkError);
|
|
1034
1050
|
// Last resort: extract user info from JWT claims in the event
|
|
1035
1051
|
try {
|
|
1036
1052
|
const claims = event.detail?.claims;
|
|
@@ -1042,7 +1058,7 @@ export class HankoAuth extends LitElement {
|
|
|
1042
1058
|
emailVerified: claims.email_verified || false,
|
|
1043
1059
|
};
|
|
1044
1060
|
userInfoRetrieved = true;
|
|
1045
|
-
this.log("
|
|
1061
|
+
this.log("User info extracted from JWT claims");
|
|
1046
1062
|
} else {
|
|
1047
1063
|
this.logError("No user claims available in event");
|
|
1048
1064
|
this.user = null;
|
|
@@ -1059,7 +1075,11 @@ export class HankoAuth extends LitElement {
|
|
|
1059
1075
|
}
|
|
1060
1076
|
}
|
|
1061
1077
|
|
|
1062
|
-
this.log("
|
|
1078
|
+
this.log("User state updated:", this.user);
|
|
1079
|
+
await this.fetchProfileDisplayName();
|
|
1080
|
+
if (this.user && this.profilePictureUrl) {
|
|
1081
|
+
this.user = { ...this.user, avatarUrl: this.profilePictureUrl };
|
|
1082
|
+
}
|
|
1063
1083
|
|
|
1064
1084
|
// Broadcast state changes to other instances
|
|
1065
1085
|
if (this._isPrimary) {
|
|
@@ -1078,12 +1098,10 @@ export class HankoAuth extends LitElement {
|
|
|
1078
1098
|
if (this.osmRequired) {
|
|
1079
1099
|
await this.checkOSMConnection();
|
|
1080
1100
|
}
|
|
1081
|
-
// Fetch profile display name (only works with login.hotosm.org backend)
|
|
1082
|
-
await this.fetchProfileDisplayName();
|
|
1083
1101
|
|
|
1084
1102
|
// Auto-connect to OSM if required and auto-connect is enabled
|
|
1085
1103
|
if (this.osmRequired && this.autoConnect && !this.osmConnected) {
|
|
1086
|
-
this.log("
|
|
1104
|
+
this.log("Auto-connecting to OSM...");
|
|
1087
1105
|
this.handleOSMConnect();
|
|
1088
1106
|
return; // Exit early - redirect will happen after OSM OAuth callback
|
|
1089
1107
|
}
|
|
@@ -1092,7 +1110,7 @@ export class HankoAuth extends LitElement {
|
|
|
1092
1110
|
const canRedirect = !this.osmRequired || this.osmConnected;
|
|
1093
1111
|
|
|
1094
1112
|
this.log(
|
|
1095
|
-
"
|
|
1113
|
+
"Checking redirect-after-login:",
|
|
1096
1114
|
this.redirectAfterLogin,
|
|
1097
1115
|
"showProfile:",
|
|
1098
1116
|
this.showProfile,
|
|
@@ -1109,13 +1127,13 @@ export class HankoAuth extends LitElement {
|
|
|
1109
1127
|
);
|
|
1110
1128
|
|
|
1111
1129
|
if (this.redirectAfterLogin) {
|
|
1112
|
-
this.log("
|
|
1130
|
+
this.log("Redirecting to:", this.redirectAfterLogin);
|
|
1113
1131
|
window.location.href = this.redirectAfterLogin;
|
|
1114
1132
|
} else {
|
|
1115
|
-
this.log("
|
|
1133
|
+
this.log("No redirect (redirectAfterLogin not set)");
|
|
1116
1134
|
}
|
|
1117
1135
|
} else {
|
|
1118
|
-
this.log("
|
|
1136
|
+
this.log("Waiting for OSM connection before redirect");
|
|
1119
1137
|
}
|
|
1120
1138
|
}
|
|
1121
1139
|
|
|
@@ -1128,7 +1146,7 @@ export class HankoAuth extends LitElement {
|
|
|
1128
1146
|
const loginPath = `${basePath}${authPath}/login`;
|
|
1129
1147
|
const fullUrl = `${loginPath}?scopes=${scopes}`;
|
|
1130
1148
|
|
|
1131
|
-
this.log("
|
|
1149
|
+
this.log("OSM Connect clicked!");
|
|
1132
1150
|
this.log(" basePath:", basePath);
|
|
1133
1151
|
this.log(" authPath:", authPath);
|
|
1134
1152
|
this.log(" Login path:", fullUrl);
|
|
@@ -1149,32 +1167,32 @@ export class HankoAuth extends LitElement {
|
|
|
1149
1167
|
if (response.status === 0 || response.type === "opaqueredirect") {
|
|
1150
1168
|
// This is a redirect response
|
|
1151
1169
|
const redirectUrl = response.headers.get("Location") || response.url;
|
|
1152
|
-
this.log("
|
|
1170
|
+
this.log("Got redirect URL:", redirectUrl);
|
|
1153
1171
|
window.location.href = redirectUrl;
|
|
1154
1172
|
} else if (response.status >= 300 && response.status < 400) {
|
|
1155
1173
|
const redirectUrl = response.headers.get("Location");
|
|
1156
|
-
this.log("
|
|
1174
|
+
this.log("Got redirect URL from header:", redirectUrl);
|
|
1157
1175
|
if (redirectUrl) {
|
|
1158
1176
|
window.location.href = redirectUrl;
|
|
1159
1177
|
}
|
|
1160
1178
|
} else {
|
|
1161
|
-
this.logError("
|
|
1179
|
+
this.logError("Unexpected response:", response.status);
|
|
1162
1180
|
const text = await response.text();
|
|
1163
1181
|
this.logError(" Response body:", text.substring(0, 200));
|
|
1164
1182
|
}
|
|
1165
1183
|
} catch (error) {
|
|
1166
|
-
this.logError("
|
|
1184
|
+
this.logError("Failed to fetch redirect URL:", error);
|
|
1167
1185
|
}
|
|
1168
1186
|
}
|
|
1169
1187
|
|
|
1170
1188
|
private async handleLogout() {
|
|
1171
|
-
this.log("
|
|
1172
|
-
this.log("
|
|
1189
|
+
this.log("Logout initiated");
|
|
1190
|
+
this.log("Current state before logout:", {
|
|
1173
1191
|
user: this.user,
|
|
1174
1192
|
osmConnected: this.osmConnected,
|
|
1175
1193
|
osmData: this.osmData,
|
|
1176
1194
|
});
|
|
1177
|
-
this.log("
|
|
1195
|
+
this.log("Cookies before logout:", document.cookie);
|
|
1178
1196
|
|
|
1179
1197
|
try {
|
|
1180
1198
|
const basePath = this.getBasePath();
|
|
@@ -1184,25 +1202,25 @@ export class HankoAuth extends LitElement {
|
|
|
1184
1202
|
const disconnectUrl = disconnectPath.startsWith("http")
|
|
1185
1203
|
? disconnectPath
|
|
1186
1204
|
: `${window.location.origin}${disconnectPath}`;
|
|
1187
|
-
this.log("
|
|
1205
|
+
this.log("Calling OSM disconnect:", disconnectUrl);
|
|
1188
1206
|
|
|
1189
1207
|
const response = await fetch(disconnectUrl, {
|
|
1190
1208
|
method: "POST",
|
|
1191
1209
|
credentials: "include",
|
|
1192
1210
|
});
|
|
1193
1211
|
|
|
1194
|
-
this.log("
|
|
1212
|
+
this.log("Disconnect response status:", response.status);
|
|
1195
1213
|
const data = await response.json();
|
|
1196
|
-
this.log("
|
|
1197
|
-
this.log("
|
|
1214
|
+
this.log("Disconnect response data:", data);
|
|
1215
|
+
this.log("OSM disconnected");
|
|
1198
1216
|
} catch (error) {
|
|
1199
|
-
this.logError("
|
|
1217
|
+
this.logError("OSM disconnect failed:", error);
|
|
1200
1218
|
}
|
|
1201
1219
|
|
|
1202
1220
|
if (this._hanko) {
|
|
1203
1221
|
try {
|
|
1204
1222
|
await this._hanko.user.logout();
|
|
1205
|
-
this.log("
|
|
1223
|
+
this.log("Hanko logout successful");
|
|
1206
1224
|
} catch (error) {
|
|
1207
1225
|
this.logError("Hanko logout failed:", error);
|
|
1208
1226
|
}
|
|
@@ -1211,19 +1229,17 @@ export class HankoAuth extends LitElement {
|
|
|
1211
1229
|
// Use shared cleanup method
|
|
1212
1230
|
this._clearAuthState();
|
|
1213
1231
|
|
|
1214
|
-
this.log(
|
|
1215
|
-
"โ
Logout complete - component will re-render with updated state",
|
|
1216
|
-
);
|
|
1232
|
+
this.log("Logout complete - component will re-render with updated state");
|
|
1217
1233
|
|
|
1218
1234
|
// Redirect after logout if configured (but not if already there)
|
|
1219
1235
|
if (this.redirectAfterLogout) {
|
|
1220
1236
|
const currentUrl = window.location.href.replace(/\/$/, "");
|
|
1221
1237
|
const targetUrl = this.redirectAfterLogout.replace(/\/$/, "");
|
|
1222
1238
|
if (currentUrl !== targetUrl && !currentUrl.startsWith(targetUrl + "#")) {
|
|
1223
|
-
this.log("
|
|
1239
|
+
this.log("Redirecting after logout to:", this.redirectAfterLogout);
|
|
1224
1240
|
window.location.href = this.redirectAfterLogout;
|
|
1225
1241
|
} else {
|
|
1226
|
-
this.log("
|
|
1242
|
+
this.log("Already on logout target, skipping redirect");
|
|
1227
1243
|
}
|
|
1228
1244
|
}
|
|
1229
1245
|
// Otherwise let Lit's reactivity handle the re-render
|
|
@@ -1239,14 +1255,14 @@ export class HankoAuth extends LitElement {
|
|
|
1239
1255
|
document.cookie = "hanko=; path=/; max-age=0";
|
|
1240
1256
|
document.cookie = `osm_connection=; path=/; domain=${hostname}; max-age=0`;
|
|
1241
1257
|
document.cookie = "osm_connection=; path=/; max-age=0";
|
|
1242
|
-
this.log("
|
|
1258
|
+
this.log("Cookies cleared");
|
|
1243
1259
|
|
|
1244
1260
|
// Clear session verification and onboarding flags
|
|
1245
1261
|
const verifyKey = getSessionVerifyKey(hostname);
|
|
1246
1262
|
const onboardingKey = getSessionOnboardingKey(hostname);
|
|
1247
1263
|
sessionStorage.removeItem(verifyKey);
|
|
1248
1264
|
sessionStorage.removeItem(onboardingKey);
|
|
1249
|
-
this.log("
|
|
1265
|
+
this.log("Session flags cleared");
|
|
1250
1266
|
|
|
1251
1267
|
// Reset state
|
|
1252
1268
|
this.user = null;
|
|
@@ -1254,6 +1270,7 @@ export class HankoAuth extends LitElement {
|
|
|
1254
1270
|
this.osmData = null;
|
|
1255
1271
|
this.hasAppMapping = false;
|
|
1256
1272
|
this.userProfileLanguage = null; // Clear user's language preference
|
|
1273
|
+
this.profilePictureUrl = ""; // Clear profile picture
|
|
1257
1274
|
|
|
1258
1275
|
// Broadcast state changes to other instances
|
|
1259
1276
|
if (this._isPrimary) {
|
|
@@ -1270,8 +1287,8 @@ export class HankoAuth extends LitElement {
|
|
|
1270
1287
|
}
|
|
1271
1288
|
|
|
1272
1289
|
private async handleSessionExpired() {
|
|
1273
|
-
this.log("
|
|
1274
|
-
this.log("
|
|
1290
|
+
this.log("Session expired event received");
|
|
1291
|
+
this.log("Current state:", {
|
|
1275
1292
|
user: this.user,
|
|
1276
1293
|
osmConnected: this.osmConnected,
|
|
1277
1294
|
loading: this.loading,
|
|
@@ -1280,18 +1297,18 @@ export class HankoAuth extends LitElement {
|
|
|
1280
1297
|
// If still loading, wait for session check to complete before acting
|
|
1281
1298
|
// The SDK may fire this event for old/stale sessions during init
|
|
1282
1299
|
if (this.loading) {
|
|
1283
|
-
this.log("
|
|
1300
|
+
this.log("Still loading, ignoring session expired event during init");
|
|
1284
1301
|
return;
|
|
1285
1302
|
}
|
|
1286
1303
|
|
|
1287
1304
|
// If we have an active user, the session is still valid
|
|
1288
1305
|
// The SDK may fire this event for old/stale sessions while a new session exists
|
|
1289
1306
|
if (this.user) {
|
|
1290
|
-
this.log("
|
|
1307
|
+
this.log("User is logged in, ignoring stale session expired event");
|
|
1291
1308
|
return;
|
|
1292
1309
|
}
|
|
1293
1310
|
|
|
1294
|
-
this.log("
|
|
1311
|
+
this.log("No active user - cleaning up state");
|
|
1295
1312
|
|
|
1296
1313
|
// Call OSM disconnect endpoint to clear httpOnly cookie
|
|
1297
1314
|
try {
|
|
@@ -1302,25 +1319,25 @@ export class HankoAuth extends LitElement {
|
|
|
1302
1319
|
const disconnectUrl = disconnectPath.startsWith("http")
|
|
1303
1320
|
? disconnectPath
|
|
1304
1321
|
: `${window.location.origin}${disconnectPath}`;
|
|
1305
|
-
this.log("
|
|
1322
|
+
this.log("Calling OSM disconnect (session expired):", disconnectUrl);
|
|
1306
1323
|
|
|
1307
1324
|
const response = await fetch(disconnectUrl, {
|
|
1308
1325
|
method: "POST",
|
|
1309
1326
|
credentials: "include",
|
|
1310
1327
|
});
|
|
1311
1328
|
|
|
1312
|
-
this.log("
|
|
1329
|
+
this.log("Disconnect response status:", response.status);
|
|
1313
1330
|
const data = await response.json();
|
|
1314
|
-
this.log("
|
|
1315
|
-
this.log("
|
|
1331
|
+
this.log("Disconnect response data:", data);
|
|
1332
|
+
this.log("OSM disconnected");
|
|
1316
1333
|
} catch (error) {
|
|
1317
|
-
this.logError("
|
|
1334
|
+
this.logError("OSM disconnect failed:", error);
|
|
1318
1335
|
}
|
|
1319
1336
|
|
|
1320
1337
|
// Use shared cleanup method
|
|
1321
1338
|
this._clearAuthState();
|
|
1322
1339
|
|
|
1323
|
-
this.log("
|
|
1340
|
+
this.log("Session cleanup complete");
|
|
1324
1341
|
|
|
1325
1342
|
// Redirect after session expired if configured (but not if already there)
|
|
1326
1343
|
if (this.redirectAfterLogout) {
|
|
@@ -1328,19 +1345,19 @@ export class HankoAuth extends LitElement {
|
|
|
1328
1345
|
const targetUrl = this.redirectAfterLogout.replace(/\/$/, "");
|
|
1329
1346
|
if (currentUrl !== targetUrl && !currentUrl.startsWith(targetUrl + "#")) {
|
|
1330
1347
|
this.log(
|
|
1331
|
-
"
|
|
1348
|
+
"Redirecting after session expired to:",
|
|
1332
1349
|
this.redirectAfterLogout,
|
|
1333
1350
|
);
|
|
1334
1351
|
window.location.href = this.redirectAfterLogout;
|
|
1335
1352
|
} else {
|
|
1336
|
-
this.log("
|
|
1353
|
+
this.log("Already on logout target, skipping redirect");
|
|
1337
1354
|
}
|
|
1338
1355
|
}
|
|
1339
1356
|
// Otherwise component will re-render and show login button
|
|
1340
1357
|
}
|
|
1341
1358
|
|
|
1342
1359
|
private handleUserLoggedOut() {
|
|
1343
|
-
this.log("
|
|
1360
|
+
this.log("User logged out in another window/tab");
|
|
1344
1361
|
// Same cleanup as session expired
|
|
1345
1362
|
this.handleSessionExpired();
|
|
1346
1363
|
}
|
|
@@ -1348,7 +1365,7 @@ export class HankoAuth extends LitElement {
|
|
|
1348
1365
|
private handleDropdownSelect(event: Event) {
|
|
1349
1366
|
const target = event.currentTarget as HTMLElement;
|
|
1350
1367
|
const action = target.dataset.action;
|
|
1351
|
-
this.log("
|
|
1368
|
+
this.log("Dropdown item selected:", action);
|
|
1352
1369
|
|
|
1353
1370
|
if (action === "profile") {
|
|
1354
1371
|
const baseUrl = this.hankoUrl;
|
|
@@ -1453,7 +1470,18 @@ export class HankoAuth extends LitElement {
|
|
|
1453
1470
|
<div class="container">
|
|
1454
1471
|
<div class="profile">
|
|
1455
1472
|
<div class="profile-header">
|
|
1456
|
-
<div class="profile-avatar"
|
|
1473
|
+
<div class="profile-avatar">
|
|
1474
|
+
${this.profilePictureUrl
|
|
1475
|
+
? html`<img
|
|
1476
|
+
class="avatar-img"
|
|
1477
|
+
src="${this.profilePictureUrl}"
|
|
1478
|
+
alt="${initial}"
|
|
1479
|
+
@error=${(e: Event) => {
|
|
1480
|
+
(e.target as HTMLImageElement).style.display = "none";
|
|
1481
|
+
}}
|
|
1482
|
+
/>`
|
|
1483
|
+
: initial}
|
|
1484
|
+
</div>
|
|
1457
1485
|
<div class="profile-info">
|
|
1458
1486
|
<div class="profile-email">
|
|
1459
1487
|
${this.user.email || this.user.id}
|
|
@@ -1538,7 +1566,18 @@ export class HankoAuth extends LitElement {
|
|
|
1538
1566
|
class="bar-trigger"
|
|
1539
1567
|
>
|
|
1540
1568
|
<div class="bar-info">
|
|
1541
|
-
<span class="header-avatar"
|
|
1569
|
+
<span class="header-avatar">
|
|
1570
|
+
${this.profilePictureUrl
|
|
1571
|
+
? html`<img
|
|
1572
|
+
class="avatar-img"
|
|
1573
|
+
src="${this.profilePictureUrl}"
|
|
1574
|
+
alt="${initial}"
|
|
1575
|
+
@error=${(e: Event) => {
|
|
1576
|
+
(e.target as HTMLImageElement).style.display = "none";
|
|
1577
|
+
}}
|
|
1578
|
+
/>`
|
|
1579
|
+
: initial}
|
|
1580
|
+
</span>
|
|
1542
1581
|
<span class="bar-email"
|
|
1543
1582
|
>${this.user.email || this.user.id}</span
|
|
1544
1583
|
>
|
|
@@ -1563,7 +1602,18 @@ export class HankoAuth extends LitElement {
|
|
|
1563
1602
|
aria-haspopup="true"
|
|
1564
1603
|
class="dropdown-trigger"
|
|
1565
1604
|
>
|
|
1566
|
-
<span class="header-avatar"
|
|
1605
|
+
<span class="header-avatar">
|
|
1606
|
+
${this.profilePictureUrl
|
|
1607
|
+
? html`<img
|
|
1608
|
+
class="avatar-img"
|
|
1609
|
+
src="${this.profilePictureUrl}"
|
|
1610
|
+
alt="${initial}"
|
|
1611
|
+
@error=${(e: Event) => {
|
|
1612
|
+
(e.target as HTMLImageElement).style.display = "none";
|
|
1613
|
+
}}
|
|
1614
|
+
/>`
|
|
1615
|
+
: initial}
|
|
1616
|
+
</span>
|
|
1567
1617
|
|
|
1568
1618
|
${this.osmConnected
|
|
1569
1619
|
? html`
|