@hotosm/hanko-auth 0.4.6 โ 0.4.8
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 +25 -9
- package/dist/hanko-auth.esm.js +632 -589
- package/dist/hanko-auth.iife.js +95 -57
- package/dist/hanko-auth.umd.js +98 -60
- package/package.json +1 -1
- package/src/hanko-auth.styles.ts +39 -22
- package/src/hanko-auth.ts +146 -116
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
|
|
|
@@ -112,26 +112,20 @@ export class HankoAuth extends LitElement {
|
|
|
112
112
|
redirectAfterLogout = "";
|
|
113
113
|
@property({ type: String, attribute: "display-name" })
|
|
114
114
|
displayNameAttr = "";
|
|
115
|
-
// URL to check if user has app mapping (for cross-app auth scenarios)
|
|
116
115
|
@property({ type: String, attribute: "mapping-check-url" }) mappingCheckUrl =
|
|
117
116
|
"";
|
|
118
|
-
// App identifier for onboarding redirect
|
|
119
117
|
@property({ type: String, attribute: "app-id" }) appId = "";
|
|
120
118
|
// Custom login page URL (for standalone mode - overrides ${hankoUrl}/app)
|
|
121
119
|
@property({ type: String, attribute: "login-url" }) loginUrl = "";
|
|
122
|
-
// Language code (en, es, fr, pt, etc.)
|
|
123
120
|
@property({ type: String, reflect: true }) lang = "en";
|
|
124
|
-
// Button variant (filled, outline, plain)
|
|
125
121
|
@property({ type: String, attribute: "button-variant" }) buttonVariant:
|
|
126
122
|
| "filled"
|
|
127
123
|
| "outline"
|
|
128
124
|
| "plain" = "plain";
|
|
129
|
-
// Button color (primary, neutral, danger)
|
|
130
125
|
@property({ type: String, attribute: "button-color" }) buttonColor:
|
|
131
126
|
| "primary"
|
|
132
127
|
| "neutral"
|
|
133
128
|
| "danger" = "primary";
|
|
134
|
-
// Display mode: "default" (compact avatar button) or "bar" (full-width bar with avatar + email + chevron)
|
|
135
129
|
@property({ type: String, reflect: true }) display: "default" | "bar" =
|
|
136
130
|
"default";
|
|
137
131
|
|
|
@@ -144,6 +138,7 @@ export class HankoAuth extends LitElement {
|
|
|
144
138
|
@state() private error: string | null = null;
|
|
145
139
|
@state() private hankoReady = false; // Tracks when Hanko registration is complete
|
|
146
140
|
@state() private profileDisplayName: string = "";
|
|
141
|
+
@state() private profilePictureUrl: string = "";
|
|
147
142
|
@state() private hasAppMapping = false; // True if user has mapping in the app
|
|
148
143
|
@state() private userProfileLanguage: string | null = null; // Language from user profile
|
|
149
144
|
// dropdown
|
|
@@ -268,12 +263,12 @@ export class HankoAuth extends LitElement {
|
|
|
268
263
|
|
|
269
264
|
// If already initialized or being initialized by another instance, sync state and skip init
|
|
270
265
|
if (sharedAuth.initialized || sharedAuth.primary) {
|
|
271
|
-
this.log("
|
|
266
|
+
this.log("Using shared state from primary instance");
|
|
272
267
|
this._syncFromShared();
|
|
273
268
|
this._isPrimary = false;
|
|
274
269
|
} else {
|
|
275
270
|
// This is the first/primary instance - claim it immediately to prevent race conditions
|
|
276
|
-
this.log("
|
|
271
|
+
this.log("This is the primary instance");
|
|
277
272
|
this._isPrimary = true;
|
|
278
273
|
sharedAuth.primary = this;
|
|
279
274
|
sharedAuth.initialized = true; // Mark as initialized immediately to prevent other instances from also initializing
|
|
@@ -298,7 +293,7 @@ export class HankoAuth extends LitElement {
|
|
|
298
293
|
if (this._isPrimary && sharedAuth.instances.size > 0) {
|
|
299
294
|
const newPrimary = sharedAuth.instances.values().next().value;
|
|
300
295
|
if (newPrimary) {
|
|
301
|
-
this.log("
|
|
296
|
+
this.log("Promoting new primary instance");
|
|
302
297
|
newPrimary._isPrimary = true;
|
|
303
298
|
sharedAuth.primary = newPrimary;
|
|
304
299
|
}
|
|
@@ -321,6 +316,8 @@ export class HankoAuth extends LitElement {
|
|
|
321
316
|
if (this._hanko !== sharedAuth.hanko) this._hanko = sharedAuth.hanko;
|
|
322
317
|
if (this.profileDisplayName !== sharedAuth.profileDisplayName)
|
|
323
318
|
this.profileDisplayName = sharedAuth.profileDisplayName;
|
|
319
|
+
if (this.profilePictureUrl !== sharedAuth.profilePictureUrl)
|
|
320
|
+
this.profilePictureUrl = sharedAuth.profilePictureUrl;
|
|
324
321
|
if (this.hankoReady !== sharedAuth.hankoReady)
|
|
325
322
|
this.hankoReady = sharedAuth.hankoReady;
|
|
326
323
|
}
|
|
@@ -332,6 +329,7 @@ export class HankoAuth extends LitElement {
|
|
|
332
329
|
sharedAuth.osmData = this.osmData;
|
|
333
330
|
sharedAuth.loading = this.loading;
|
|
334
331
|
sharedAuth.profileDisplayName = this.profileDisplayName;
|
|
332
|
+
sharedAuth.profilePictureUrl = this.profilePictureUrl;
|
|
335
333
|
sharedAuth.hankoReady = this.hankoReady;
|
|
336
334
|
|
|
337
335
|
// Sync to all other instances
|
|
@@ -349,7 +347,7 @@ export class HankoAuth extends LitElement {
|
|
|
349
347
|
if (!document.hidden && !this.showProfile && !this.user) {
|
|
350
348
|
// Page became visible, we're in header mode, and no user is logged in
|
|
351
349
|
// Re-check session in case user logged in elsewhere
|
|
352
|
-
this.log("
|
|
350
|
+
this.log("Page visible, re-checking session...");
|
|
353
351
|
this.checkSession();
|
|
354
352
|
}
|
|
355
353
|
};
|
|
@@ -402,13 +400,8 @@ export class HankoAuth extends LitElement {
|
|
|
402
400
|
}
|
|
403
401
|
}
|
|
404
402
|
|
|
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
|
-
*/
|
|
403
|
+
/* Translations */
|
|
410
404
|
private t(key: keyof typeof translations.en): string {
|
|
411
|
-
// When user is logged in, use their profile language
|
|
412
405
|
const effectiveLang =
|
|
413
406
|
this.user && this.userProfileLanguage
|
|
414
407
|
? this.userProfileLanguage
|
|
@@ -547,12 +540,12 @@ export class HankoAuth extends LitElement {
|
|
|
547
540
|
this.log("๐ Checking for existing Hanko session...");
|
|
548
541
|
|
|
549
542
|
if (!this._hanko) {
|
|
550
|
-
this.log("
|
|
543
|
+
this.log("Hanko instance not initialized yet");
|
|
551
544
|
return;
|
|
552
545
|
}
|
|
553
546
|
|
|
554
547
|
try {
|
|
555
|
-
this.log("
|
|
548
|
+
this.log("Checking session validity via cookie...");
|
|
556
549
|
|
|
557
550
|
// First, try to validate the session cookie directly with Hanko
|
|
558
551
|
// This works across subdomains because the cookie has domain: .hotosm.test
|
|
@@ -574,13 +567,13 @@ export class HankoAuth extends LitElement {
|
|
|
574
567
|
// Check if session is actually valid (endpoint returns 200 with is_valid:false when no session)
|
|
575
568
|
if (sessionData.is_valid === false) {
|
|
576
569
|
this.log(
|
|
577
|
-
"
|
|
570
|
+
"Session validation returned is_valid:false - no valid session",
|
|
578
571
|
);
|
|
579
572
|
return;
|
|
580
573
|
}
|
|
581
574
|
|
|
582
|
-
this.log("
|
|
583
|
-
this.log("
|
|
575
|
+
this.log("Valid Hanko session found via cookie");
|
|
576
|
+
this.log("Session data:", sessionData);
|
|
584
577
|
|
|
585
578
|
// Now get the full user data from the login backend /me endpoint
|
|
586
579
|
// This endpoint validates the JWT and returns complete user info
|
|
@@ -596,7 +589,7 @@ export class HankoAuth extends LitElement {
|
|
|
596
589
|
let needsSdkFallback = true;
|
|
597
590
|
if (meResponse.ok) {
|
|
598
591
|
const userData = await meResponse.json();
|
|
599
|
-
this.log("
|
|
592
|
+
this.log("User data retrieved from /me:", userData);
|
|
600
593
|
|
|
601
594
|
// Only use /me if it has email (login.hotosm.org has it, Hanko vanilla doesn't)
|
|
602
595
|
if (userData.email) {
|
|
@@ -609,12 +602,12 @@ export class HankoAuth extends LitElement {
|
|
|
609
602
|
};
|
|
610
603
|
needsSdkFallback = false;
|
|
611
604
|
} else {
|
|
612
|
-
this.log("
|
|
605
|
+
this.log("/me has no email, will use SDK fallback");
|
|
613
606
|
}
|
|
614
607
|
}
|
|
615
608
|
|
|
616
609
|
if (needsSdkFallback) {
|
|
617
|
-
this.log("
|
|
610
|
+
this.log("Using SDK to get user with email");
|
|
618
611
|
// Fallback to SDK method which has email
|
|
619
612
|
const user = await this._hanko.user.getCurrent();
|
|
620
613
|
this.user = {
|
|
@@ -625,7 +618,7 @@ export class HankoAuth extends LitElement {
|
|
|
625
618
|
};
|
|
626
619
|
}
|
|
627
620
|
} catch (userError) {
|
|
628
|
-
this.log("
|
|
621
|
+
this.log("Failed to get user data:", userError);
|
|
629
622
|
// Last resort: use session data if available
|
|
630
623
|
if (sessionData.user_id) {
|
|
631
624
|
this.user = {
|
|
@@ -650,7 +643,7 @@ export class HankoAuth extends LitElement {
|
|
|
650
643
|
!alreadyVerified
|
|
651
644
|
) {
|
|
652
645
|
this.log(
|
|
653
|
-
"
|
|
646
|
+
"verify-session enabled, redirecting to callback for app verification...",
|
|
654
647
|
);
|
|
655
648
|
sessionStorage.setItem(verifyKey, "true");
|
|
656
649
|
window.location.href = this.redirectAfterLogin;
|
|
@@ -687,20 +680,20 @@ export class HankoAuth extends LitElement {
|
|
|
687
680
|
// Fetch profile display name
|
|
688
681
|
await this.fetchProfileDisplayName();
|
|
689
682
|
if (this.osmRequired && this.autoConnect && !this.osmConnected) {
|
|
690
|
-
this.log("
|
|
683
|
+
this.log("Auto-connecting to OSM (from existing session)...");
|
|
691
684
|
this.handleOSMConnect();
|
|
692
685
|
}
|
|
693
686
|
}
|
|
694
687
|
} else {
|
|
695
|
-
this.log("
|
|
688
|
+
this.log("No valid session cookie found - user needs to login");
|
|
696
689
|
}
|
|
697
690
|
} catch (validateError) {
|
|
698
|
-
this.log("
|
|
699
|
-
this.log("
|
|
691
|
+
this.log("Session validation failed:", validateError);
|
|
692
|
+
this.log("No valid session - user needs to login");
|
|
700
693
|
}
|
|
701
694
|
} catch (error) {
|
|
702
|
-
this.log("
|
|
703
|
-
this.log("
|
|
695
|
+
this.log("Session check error:", error);
|
|
696
|
+
this.log("No existing session - user needs to login");
|
|
704
697
|
} finally {
|
|
705
698
|
// Broadcast state changes to other instances
|
|
706
699
|
if (this._isPrimary) {
|
|
@@ -712,12 +705,12 @@ export class HankoAuth extends LitElement {
|
|
|
712
705
|
private async checkOSMConnection() {
|
|
713
706
|
// Skip OSM check if not required
|
|
714
707
|
if (!this.osmRequired) {
|
|
715
|
-
this.log("
|
|
708
|
+
this.log("OSM not required, skipping connection check");
|
|
716
709
|
return;
|
|
717
710
|
}
|
|
718
711
|
|
|
719
712
|
if (this.osmConnected) {
|
|
720
|
-
this.log("
|
|
713
|
+
this.log("Already connected to OSM, skipping check");
|
|
721
714
|
return;
|
|
722
715
|
}
|
|
723
716
|
|
|
@@ -737,23 +730,23 @@ export class HankoAuth extends LitElement {
|
|
|
737
730
|
const statusPath = `${basePath}${authPath}/status`;
|
|
738
731
|
const statusUrl = `${statusPath}`; // Relative URL for proxy
|
|
739
732
|
|
|
740
|
-
this.log("
|
|
733
|
+
this.log("Checking OSM connection at:", statusUrl);
|
|
741
734
|
this.log(" basePath:", basePath);
|
|
742
735
|
this.log(" authPath:", authPath);
|
|
743
|
-
this.log("
|
|
736
|
+
this.log("Current cookies:", document.cookie);
|
|
744
737
|
|
|
745
738
|
const response = await fetch(statusUrl, {
|
|
746
739
|
credentials: "include",
|
|
747
740
|
redirect: "follow",
|
|
748
741
|
});
|
|
749
742
|
|
|
750
|
-
this.log("
|
|
751
|
-
this.log("
|
|
752
|
-
this.log("
|
|
743
|
+
this.log("OSM status response:", response.status);
|
|
744
|
+
this.log("Final URL after redirects:", response.url);
|
|
745
|
+
this.log("Response headers:", [...response.headers.entries()]);
|
|
753
746
|
|
|
754
747
|
if (response.ok) {
|
|
755
748
|
const text = await response.text();
|
|
756
|
-
this.log("
|
|
749
|
+
this.log("OSM raw response:", text.substring(0, 200));
|
|
757
750
|
|
|
758
751
|
let data;
|
|
759
752
|
try {
|
|
@@ -766,10 +759,10 @@ export class HankoAuth extends LitElement {
|
|
|
766
759
|
throw new Error("Invalid JSON response from OSM status endpoint");
|
|
767
760
|
}
|
|
768
761
|
|
|
769
|
-
this.log("
|
|
762
|
+
this.log("OSM status data:", data);
|
|
770
763
|
|
|
771
764
|
if (data.connected) {
|
|
772
|
-
this.log("
|
|
765
|
+
this.log("OSM is connected:", data.osm_username);
|
|
773
766
|
this.osmConnected = true;
|
|
774
767
|
this.osmData = data;
|
|
775
768
|
|
|
@@ -786,7 +779,7 @@ export class HankoAuth extends LitElement {
|
|
|
786
779
|
// The Login page's onboarding flow listens for 'osm-connected' event
|
|
787
780
|
// and handles the redirect to the app's onboarding endpoint
|
|
788
781
|
} else {
|
|
789
|
-
this.log("
|
|
782
|
+
this.log("OSM is NOT connected");
|
|
790
783
|
this.osmConnected = false;
|
|
791
784
|
this.osmData = null;
|
|
792
785
|
}
|
|
@@ -816,12 +809,12 @@ export class HankoAuth extends LitElement {
|
|
|
816
809
|
const onboardingKey = getSessionOnboardingKey(window.location.hostname);
|
|
817
810
|
const onboardingCompleted = sessionStorage.getItem(onboardingKey);
|
|
818
811
|
if (onboardingCompleted) {
|
|
819
|
-
this.log("
|
|
812
|
+
this.log("Onboarding already completed this session, skipping check");
|
|
820
813
|
this.hasAppMapping = true;
|
|
821
814
|
return true;
|
|
822
815
|
}
|
|
823
816
|
|
|
824
|
-
this.log("
|
|
817
|
+
this.log("Checking app mapping at:", this.mappingCheckUrl);
|
|
825
818
|
|
|
826
819
|
try {
|
|
827
820
|
const response = await fetch(this.mappingCheckUrl, {
|
|
@@ -830,12 +823,12 @@ export class HankoAuth extends LitElement {
|
|
|
830
823
|
|
|
831
824
|
if (response.ok) {
|
|
832
825
|
const data = await response.json();
|
|
833
|
-
this.log("
|
|
826
|
+
this.log("Mapping check response:", data);
|
|
834
827
|
|
|
835
828
|
if (data.needs_onboarding) {
|
|
836
829
|
// User has Hanko session but no app mapping - redirect to onboarding
|
|
837
830
|
// Don't set flag here - only set it when onboarding completes
|
|
838
|
-
this.log("
|
|
831
|
+
this.log("User needs onboarding, redirecting...");
|
|
839
832
|
const returnTo = encodeURIComponent(window.location.origin);
|
|
840
833
|
const appParam = this.appId ? `onboarding=${this.appId}` : "";
|
|
841
834
|
window.location.href = `${this.hankoUrl}/app?${appParam}&return_to=${returnTo}`;
|
|
@@ -845,11 +838,11 @@ export class HankoAuth extends LitElement {
|
|
|
845
838
|
// User has mapping - mark onboarding as completed
|
|
846
839
|
sessionStorage.setItem(onboardingKey, "true");
|
|
847
840
|
this.hasAppMapping = true;
|
|
848
|
-
this.log("
|
|
841
|
+
this.log("User has app mapping, onboarding marked complete");
|
|
849
842
|
return true;
|
|
850
843
|
} else if (response.status === 401 || response.status === 403) {
|
|
851
844
|
// Needs onboarding
|
|
852
|
-
this.log("
|
|
845
|
+
this.log("401/403 - User needs onboarding, redirecting...");
|
|
853
846
|
const returnTo = encodeURIComponent(window.location.origin);
|
|
854
847
|
const appParam = this.appId ? `onboarding=${this.appId}` : "";
|
|
855
848
|
window.location.href = `${this.hankoUrl}/app?${appParam}&return_to=${returnTo}`;
|
|
@@ -857,10 +850,10 @@ export class HankoAuth extends LitElement {
|
|
|
857
850
|
}
|
|
858
851
|
|
|
859
852
|
// Other status codes - proceed without blocking
|
|
860
|
-
this.log("
|
|
853
|
+
this.log("Unexpected status from mapping check:", response.status);
|
|
861
854
|
return true;
|
|
862
855
|
} catch (error) {
|
|
863
|
-
this.log("
|
|
856
|
+
this.log("App mapping check failed:", error);
|
|
864
857
|
// Don't block the user, just log the error
|
|
865
858
|
return true;
|
|
866
859
|
}
|
|
@@ -870,7 +863,7 @@ export class HankoAuth extends LitElement {
|
|
|
870
863
|
private async fetchProfileDisplayName() {
|
|
871
864
|
try {
|
|
872
865
|
const profileUrl = `${this.hankoUrl}/api/profile/me`;
|
|
873
|
-
this.log("
|
|
866
|
+
this.log("Fetching profile from:", profileUrl);
|
|
874
867
|
|
|
875
868
|
const response = await fetch(profileUrl, {
|
|
876
869
|
credentials: "include",
|
|
@@ -878,22 +871,29 @@ export class HankoAuth extends LitElement {
|
|
|
878
871
|
|
|
879
872
|
if (response.ok) {
|
|
880
873
|
const profile = await response.json();
|
|
881
|
-
this.log("
|
|
874
|
+
this.log("Profile data:", profile);
|
|
882
875
|
|
|
883
876
|
if (profile.first_name || profile.last_name) {
|
|
884
877
|
this.profileDisplayName =
|
|
885
878
|
`${profile.first_name || ""} ${profile.last_name || ""}`.trim();
|
|
886
|
-
this.log("
|
|
879
|
+
this.log("Display name set to:", this.profileDisplayName);
|
|
880
|
+
}
|
|
881
|
+
|
|
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
887
|
}
|
|
888
888
|
|
|
889
889
|
// Set language from user profile if available
|
|
890
890
|
if (profile.language) {
|
|
891
891
|
this.userProfileLanguage = profile.language;
|
|
892
|
-
this.log("
|
|
892
|
+
this.log("Language set from profile:", this.userProfileLanguage);
|
|
893
893
|
}
|
|
894
894
|
}
|
|
895
895
|
} catch (error) {
|
|
896
|
-
this.log("
|
|
896
|
+
this.log("Could not fetch profile:", error);
|
|
897
897
|
}
|
|
898
898
|
}
|
|
899
899
|
|
|
@@ -922,7 +922,7 @@ export class HankoAuth extends LitElement {
|
|
|
922
922
|
|
|
923
923
|
// Skip if already attached to the same element
|
|
924
924
|
if (hankoAuth && hankoAuth === this._currentHankoAuthElement) {
|
|
925
|
-
this.log("
|
|
925
|
+
this.log("Event listeners already attached to this element");
|
|
926
926
|
return;
|
|
927
927
|
}
|
|
928
928
|
|
|
@@ -931,11 +931,11 @@ export class HankoAuth extends LitElement {
|
|
|
931
931
|
this.log("๐ฏ Attaching event listeners to hanko-auth element");
|
|
932
932
|
|
|
933
933
|
hankoAuth.addEventListener("onSessionCreated", (e: any) => {
|
|
934
|
-
this.log(
|
|
934
|
+
this.log(`Hanko event: onSessionCreated`, e.detail);
|
|
935
935
|
|
|
936
936
|
const sessionId = e.detail?.claims?.session_id;
|
|
937
937
|
if (sessionId && this._lastSessionId === sessionId) {
|
|
938
|
-
this.log("
|
|
938
|
+
this.log("Skipping duplicate session event");
|
|
939
939
|
return;
|
|
940
940
|
}
|
|
941
941
|
this._lastSessionId = sessionId;
|
|
@@ -981,7 +981,7 @@ export class HankoAuth extends LitElement {
|
|
|
981
981
|
|
|
982
982
|
if (meResponse.ok) {
|
|
983
983
|
const userData = await meResponse.json();
|
|
984
|
-
this.log("
|
|
984
|
+
this.log("User data retrieved from /me:", userData);
|
|
985
985
|
|
|
986
986
|
// Only use /me if it has email (login.hotosm.org has it, Hanko vanilla doesn't)
|
|
987
987
|
if (userData.email) {
|
|
@@ -994,17 +994,15 @@ export class HankoAuth extends LitElement {
|
|
|
994
994
|
};
|
|
995
995
|
userInfoRetrieved = true;
|
|
996
996
|
} else {
|
|
997
|
-
this.log("
|
|
997
|
+
this.log("/me has no email, will try SDK fallback");
|
|
998
998
|
}
|
|
999
999
|
} else {
|
|
1000
|
-
this.log(
|
|
1001
|
-
"โ ๏ธ /me endpoint returned non-OK status, will try SDK fallback",
|
|
1002
|
-
);
|
|
1000
|
+
this.log("/me endpoint returned non-OK status, will try SDK fallback");
|
|
1003
1001
|
}
|
|
1004
1002
|
} catch (error) {
|
|
1005
1003
|
// NetworkError or timeout on cross-origin fetch is common with mkcert certs
|
|
1006
1004
|
this.log(
|
|
1007
|
-
"
|
|
1005
|
+
"/me endpoint fetch failed (timeout or cross-origin TLS issue):",
|
|
1008
1006
|
error,
|
|
1009
1007
|
);
|
|
1010
1008
|
}
|
|
@@ -1012,7 +1010,7 @@ export class HankoAuth extends LitElement {
|
|
|
1012
1010
|
// Fallback to SDK method if /me didn't work
|
|
1013
1011
|
if (!userInfoRetrieved) {
|
|
1014
1012
|
try {
|
|
1015
|
-
this.log("
|
|
1013
|
+
this.log("Trying SDK fallback for user info...");
|
|
1016
1014
|
// Add timeout to SDK call in case it hangs
|
|
1017
1015
|
const timeoutPromise = new Promise((_, reject) =>
|
|
1018
1016
|
setTimeout(() => reject(new Error("SDK timeout")), 5000),
|
|
@@ -1028,9 +1026,9 @@ export class HankoAuth extends LitElement {
|
|
|
1028
1026
|
emailVerified: user.email_verified || false,
|
|
1029
1027
|
};
|
|
1030
1028
|
userInfoRetrieved = true;
|
|
1031
|
-
this.log("
|
|
1029
|
+
this.log("User info retrieved via SDK fallback");
|
|
1032
1030
|
} catch (sdkError) {
|
|
1033
|
-
this.log("
|
|
1031
|
+
this.log("SDK fallback failed, trying JWT claims:", sdkError);
|
|
1034
1032
|
// Last resort: extract user info from JWT claims in the event
|
|
1035
1033
|
try {
|
|
1036
1034
|
const claims = event.detail?.claims;
|
|
@@ -1042,7 +1040,7 @@ export class HankoAuth extends LitElement {
|
|
|
1042
1040
|
emailVerified: claims.email_verified || false,
|
|
1043
1041
|
};
|
|
1044
1042
|
userInfoRetrieved = true;
|
|
1045
|
-
this.log("
|
|
1043
|
+
this.log("User info extracted from JWT claims");
|
|
1046
1044
|
} else {
|
|
1047
1045
|
this.logError("No user claims available in event");
|
|
1048
1046
|
this.user = null;
|
|
@@ -1059,7 +1057,7 @@ export class HankoAuth extends LitElement {
|
|
|
1059
1057
|
}
|
|
1060
1058
|
}
|
|
1061
1059
|
|
|
1062
|
-
this.log("
|
|
1060
|
+
this.log("User state updated:", this.user);
|
|
1063
1061
|
|
|
1064
1062
|
// Broadcast state changes to other instances
|
|
1065
1063
|
if (this._isPrimary) {
|
|
@@ -1083,7 +1081,7 @@ export class HankoAuth extends LitElement {
|
|
|
1083
1081
|
|
|
1084
1082
|
// Auto-connect to OSM if required and auto-connect is enabled
|
|
1085
1083
|
if (this.osmRequired && this.autoConnect && !this.osmConnected) {
|
|
1086
|
-
this.log("
|
|
1084
|
+
this.log("Auto-connecting to OSM...");
|
|
1087
1085
|
this.handleOSMConnect();
|
|
1088
1086
|
return; // Exit early - redirect will happen after OSM OAuth callback
|
|
1089
1087
|
}
|
|
@@ -1092,7 +1090,7 @@ export class HankoAuth extends LitElement {
|
|
|
1092
1090
|
const canRedirect = !this.osmRequired || this.osmConnected;
|
|
1093
1091
|
|
|
1094
1092
|
this.log(
|
|
1095
|
-
"
|
|
1093
|
+
"Checking redirect-after-login:",
|
|
1096
1094
|
this.redirectAfterLogin,
|
|
1097
1095
|
"showProfile:",
|
|
1098
1096
|
this.showProfile,
|
|
@@ -1109,13 +1107,13 @@ export class HankoAuth extends LitElement {
|
|
|
1109
1107
|
);
|
|
1110
1108
|
|
|
1111
1109
|
if (this.redirectAfterLogin) {
|
|
1112
|
-
this.log("
|
|
1110
|
+
this.log("Redirecting to:", this.redirectAfterLogin);
|
|
1113
1111
|
window.location.href = this.redirectAfterLogin;
|
|
1114
1112
|
} else {
|
|
1115
|
-
this.log("
|
|
1113
|
+
this.log("No redirect (redirectAfterLogin not set)");
|
|
1116
1114
|
}
|
|
1117
1115
|
} else {
|
|
1118
|
-
this.log("
|
|
1116
|
+
this.log("Waiting for OSM connection before redirect");
|
|
1119
1117
|
}
|
|
1120
1118
|
}
|
|
1121
1119
|
|
|
@@ -1128,7 +1126,7 @@ export class HankoAuth extends LitElement {
|
|
|
1128
1126
|
const loginPath = `${basePath}${authPath}/login`;
|
|
1129
1127
|
const fullUrl = `${loginPath}?scopes=${scopes}`;
|
|
1130
1128
|
|
|
1131
|
-
this.log("
|
|
1129
|
+
this.log("OSM Connect clicked!");
|
|
1132
1130
|
this.log(" basePath:", basePath);
|
|
1133
1131
|
this.log(" authPath:", authPath);
|
|
1134
1132
|
this.log(" Login path:", fullUrl);
|
|
@@ -1149,32 +1147,32 @@ export class HankoAuth extends LitElement {
|
|
|
1149
1147
|
if (response.status === 0 || response.type === "opaqueredirect") {
|
|
1150
1148
|
// This is a redirect response
|
|
1151
1149
|
const redirectUrl = response.headers.get("Location") || response.url;
|
|
1152
|
-
this.log("
|
|
1150
|
+
this.log("Got redirect URL:", redirectUrl);
|
|
1153
1151
|
window.location.href = redirectUrl;
|
|
1154
1152
|
} else if (response.status >= 300 && response.status < 400) {
|
|
1155
1153
|
const redirectUrl = response.headers.get("Location");
|
|
1156
|
-
this.log("
|
|
1154
|
+
this.log("Got redirect URL from header:", redirectUrl);
|
|
1157
1155
|
if (redirectUrl) {
|
|
1158
1156
|
window.location.href = redirectUrl;
|
|
1159
1157
|
}
|
|
1160
1158
|
} else {
|
|
1161
|
-
this.logError("
|
|
1159
|
+
this.logError("Unexpected response:", response.status);
|
|
1162
1160
|
const text = await response.text();
|
|
1163
1161
|
this.logError(" Response body:", text.substring(0, 200));
|
|
1164
1162
|
}
|
|
1165
1163
|
} catch (error) {
|
|
1166
|
-
this.logError("
|
|
1164
|
+
this.logError("Failed to fetch redirect URL:", error);
|
|
1167
1165
|
}
|
|
1168
1166
|
}
|
|
1169
1167
|
|
|
1170
1168
|
private async handleLogout() {
|
|
1171
|
-
this.log("
|
|
1172
|
-
this.log("
|
|
1169
|
+
this.log("Logout initiated");
|
|
1170
|
+
this.log("Current state before logout:", {
|
|
1173
1171
|
user: this.user,
|
|
1174
1172
|
osmConnected: this.osmConnected,
|
|
1175
1173
|
osmData: this.osmData,
|
|
1176
1174
|
});
|
|
1177
|
-
this.log("
|
|
1175
|
+
this.log("Cookies before logout:", document.cookie);
|
|
1178
1176
|
|
|
1179
1177
|
try {
|
|
1180
1178
|
const basePath = this.getBasePath();
|
|
@@ -1184,25 +1182,25 @@ export class HankoAuth extends LitElement {
|
|
|
1184
1182
|
const disconnectUrl = disconnectPath.startsWith("http")
|
|
1185
1183
|
? disconnectPath
|
|
1186
1184
|
: `${window.location.origin}${disconnectPath}`;
|
|
1187
|
-
this.log("
|
|
1185
|
+
this.log("Calling OSM disconnect:", disconnectUrl);
|
|
1188
1186
|
|
|
1189
1187
|
const response = await fetch(disconnectUrl, {
|
|
1190
1188
|
method: "POST",
|
|
1191
1189
|
credentials: "include",
|
|
1192
1190
|
});
|
|
1193
1191
|
|
|
1194
|
-
this.log("
|
|
1192
|
+
this.log("Disconnect response status:", response.status);
|
|
1195
1193
|
const data = await response.json();
|
|
1196
|
-
this.log("
|
|
1197
|
-
this.log("
|
|
1194
|
+
this.log("Disconnect response data:", data);
|
|
1195
|
+
this.log("OSM disconnected");
|
|
1198
1196
|
} catch (error) {
|
|
1199
|
-
this.logError("
|
|
1197
|
+
this.logError("OSM disconnect failed:", error);
|
|
1200
1198
|
}
|
|
1201
1199
|
|
|
1202
1200
|
if (this._hanko) {
|
|
1203
1201
|
try {
|
|
1204
1202
|
await this._hanko.user.logout();
|
|
1205
|
-
this.log("
|
|
1203
|
+
this.log("Hanko logout successful");
|
|
1206
1204
|
} catch (error) {
|
|
1207
1205
|
this.logError("Hanko logout failed:", error);
|
|
1208
1206
|
}
|
|
@@ -1211,19 +1209,17 @@ export class HankoAuth extends LitElement {
|
|
|
1211
1209
|
// Use shared cleanup method
|
|
1212
1210
|
this._clearAuthState();
|
|
1213
1211
|
|
|
1214
|
-
this.log(
|
|
1215
|
-
"โ
Logout complete - component will re-render with updated state",
|
|
1216
|
-
);
|
|
1212
|
+
this.log("Logout complete - component will re-render with updated state");
|
|
1217
1213
|
|
|
1218
1214
|
// Redirect after logout if configured (but not if already there)
|
|
1219
1215
|
if (this.redirectAfterLogout) {
|
|
1220
1216
|
const currentUrl = window.location.href.replace(/\/$/, "");
|
|
1221
1217
|
const targetUrl = this.redirectAfterLogout.replace(/\/$/, "");
|
|
1222
1218
|
if (currentUrl !== targetUrl && !currentUrl.startsWith(targetUrl + "#")) {
|
|
1223
|
-
this.log("
|
|
1219
|
+
this.log("Redirecting after logout to:", this.redirectAfterLogout);
|
|
1224
1220
|
window.location.href = this.redirectAfterLogout;
|
|
1225
1221
|
} else {
|
|
1226
|
-
this.log("
|
|
1222
|
+
this.log("Already on logout target, skipping redirect");
|
|
1227
1223
|
}
|
|
1228
1224
|
}
|
|
1229
1225
|
// Otherwise let Lit's reactivity handle the re-render
|
|
@@ -1239,14 +1235,14 @@ export class HankoAuth extends LitElement {
|
|
|
1239
1235
|
document.cookie = "hanko=; path=/; max-age=0";
|
|
1240
1236
|
document.cookie = `osm_connection=; path=/; domain=${hostname}; max-age=0`;
|
|
1241
1237
|
document.cookie = "osm_connection=; path=/; max-age=0";
|
|
1242
|
-
this.log("
|
|
1238
|
+
this.log("Cookies cleared");
|
|
1243
1239
|
|
|
1244
1240
|
// Clear session verification and onboarding flags
|
|
1245
1241
|
const verifyKey = getSessionVerifyKey(hostname);
|
|
1246
1242
|
const onboardingKey = getSessionOnboardingKey(hostname);
|
|
1247
1243
|
sessionStorage.removeItem(verifyKey);
|
|
1248
1244
|
sessionStorage.removeItem(onboardingKey);
|
|
1249
|
-
this.log("
|
|
1245
|
+
this.log("Session flags cleared");
|
|
1250
1246
|
|
|
1251
1247
|
// Reset state
|
|
1252
1248
|
this.user = null;
|
|
@@ -1254,6 +1250,7 @@ export class HankoAuth extends LitElement {
|
|
|
1254
1250
|
this.osmData = null;
|
|
1255
1251
|
this.hasAppMapping = false;
|
|
1256
1252
|
this.userProfileLanguage = null; // Clear user's language preference
|
|
1253
|
+
this.profilePictureUrl = ""; // Clear profile picture
|
|
1257
1254
|
|
|
1258
1255
|
// Broadcast state changes to other instances
|
|
1259
1256
|
if (this._isPrimary) {
|
|
@@ -1270,8 +1267,8 @@ export class HankoAuth extends LitElement {
|
|
|
1270
1267
|
}
|
|
1271
1268
|
|
|
1272
1269
|
private async handleSessionExpired() {
|
|
1273
|
-
this.log("
|
|
1274
|
-
this.log("
|
|
1270
|
+
this.log("Session expired event received");
|
|
1271
|
+
this.log("Current state:", {
|
|
1275
1272
|
user: this.user,
|
|
1276
1273
|
osmConnected: this.osmConnected,
|
|
1277
1274
|
loading: this.loading,
|
|
@@ -1280,18 +1277,18 @@ export class HankoAuth extends LitElement {
|
|
|
1280
1277
|
// If still loading, wait for session check to complete before acting
|
|
1281
1278
|
// The SDK may fire this event for old/stale sessions during init
|
|
1282
1279
|
if (this.loading) {
|
|
1283
|
-
this.log("
|
|
1280
|
+
this.log("Still loading, ignoring session expired event during init");
|
|
1284
1281
|
return;
|
|
1285
1282
|
}
|
|
1286
1283
|
|
|
1287
1284
|
// If we have an active user, the session is still valid
|
|
1288
1285
|
// The SDK may fire this event for old/stale sessions while a new session exists
|
|
1289
1286
|
if (this.user) {
|
|
1290
|
-
this.log("
|
|
1287
|
+
this.log("User is logged in, ignoring stale session expired event");
|
|
1291
1288
|
return;
|
|
1292
1289
|
}
|
|
1293
1290
|
|
|
1294
|
-
this.log("
|
|
1291
|
+
this.log("No active user - cleaning up state");
|
|
1295
1292
|
|
|
1296
1293
|
// Call OSM disconnect endpoint to clear httpOnly cookie
|
|
1297
1294
|
try {
|
|
@@ -1302,25 +1299,25 @@ export class HankoAuth extends LitElement {
|
|
|
1302
1299
|
const disconnectUrl = disconnectPath.startsWith("http")
|
|
1303
1300
|
? disconnectPath
|
|
1304
1301
|
: `${window.location.origin}${disconnectPath}`;
|
|
1305
|
-
this.log("
|
|
1302
|
+
this.log("Calling OSM disconnect (session expired):", disconnectUrl);
|
|
1306
1303
|
|
|
1307
1304
|
const response = await fetch(disconnectUrl, {
|
|
1308
1305
|
method: "POST",
|
|
1309
1306
|
credentials: "include",
|
|
1310
1307
|
});
|
|
1311
1308
|
|
|
1312
|
-
this.log("
|
|
1309
|
+
this.log("Disconnect response status:", response.status);
|
|
1313
1310
|
const data = await response.json();
|
|
1314
|
-
this.log("
|
|
1315
|
-
this.log("
|
|
1311
|
+
this.log("Disconnect response data:", data);
|
|
1312
|
+
this.log("OSM disconnected");
|
|
1316
1313
|
} catch (error) {
|
|
1317
|
-
this.logError("
|
|
1314
|
+
this.logError("OSM disconnect failed:", error);
|
|
1318
1315
|
}
|
|
1319
1316
|
|
|
1320
1317
|
// Use shared cleanup method
|
|
1321
1318
|
this._clearAuthState();
|
|
1322
1319
|
|
|
1323
|
-
this.log("
|
|
1320
|
+
this.log("Session cleanup complete");
|
|
1324
1321
|
|
|
1325
1322
|
// Redirect after session expired if configured (but not if already there)
|
|
1326
1323
|
if (this.redirectAfterLogout) {
|
|
@@ -1328,19 +1325,19 @@ export class HankoAuth extends LitElement {
|
|
|
1328
1325
|
const targetUrl = this.redirectAfterLogout.replace(/\/$/, "");
|
|
1329
1326
|
if (currentUrl !== targetUrl && !currentUrl.startsWith(targetUrl + "#")) {
|
|
1330
1327
|
this.log(
|
|
1331
|
-
"
|
|
1328
|
+
"Redirecting after session expired to:",
|
|
1332
1329
|
this.redirectAfterLogout,
|
|
1333
1330
|
);
|
|
1334
1331
|
window.location.href = this.redirectAfterLogout;
|
|
1335
1332
|
} else {
|
|
1336
|
-
this.log("
|
|
1333
|
+
this.log("Already on logout target, skipping redirect");
|
|
1337
1334
|
}
|
|
1338
1335
|
}
|
|
1339
1336
|
// Otherwise component will re-render and show login button
|
|
1340
1337
|
}
|
|
1341
1338
|
|
|
1342
1339
|
private handleUserLoggedOut() {
|
|
1343
|
-
this.log("
|
|
1340
|
+
this.log("User logged out in another window/tab");
|
|
1344
1341
|
// Same cleanup as session expired
|
|
1345
1342
|
this.handleSessionExpired();
|
|
1346
1343
|
}
|
|
@@ -1348,7 +1345,7 @@ export class HankoAuth extends LitElement {
|
|
|
1348
1345
|
private handleDropdownSelect(event: Event) {
|
|
1349
1346
|
const target = event.currentTarget as HTMLElement;
|
|
1350
1347
|
const action = target.dataset.action;
|
|
1351
|
-
this.log("
|
|
1348
|
+
this.log("Dropdown item selected:", action);
|
|
1352
1349
|
|
|
1353
1350
|
if (action === "profile") {
|
|
1354
1351
|
const baseUrl = this.hankoUrl;
|
|
@@ -1453,7 +1450,18 @@ export class HankoAuth extends LitElement {
|
|
|
1453
1450
|
<div class="container">
|
|
1454
1451
|
<div class="profile">
|
|
1455
1452
|
<div class="profile-header">
|
|
1456
|
-
<div class="profile-avatar"
|
|
1453
|
+
<div class="profile-avatar">
|
|
1454
|
+
${this.profilePictureUrl
|
|
1455
|
+
? html`<img
|
|
1456
|
+
class="avatar-img"
|
|
1457
|
+
src="${this.profilePictureUrl}"
|
|
1458
|
+
alt="${initial}"
|
|
1459
|
+
@error=${(e: Event) => {
|
|
1460
|
+
(e.target as HTMLImageElement).style.display = "none";
|
|
1461
|
+
}}
|
|
1462
|
+
/>`
|
|
1463
|
+
: initial}
|
|
1464
|
+
</div>
|
|
1457
1465
|
<div class="profile-info">
|
|
1458
1466
|
<div class="profile-email">
|
|
1459
1467
|
${this.user.email || this.user.id}
|
|
@@ -1538,7 +1546,18 @@ export class HankoAuth extends LitElement {
|
|
|
1538
1546
|
class="bar-trigger"
|
|
1539
1547
|
>
|
|
1540
1548
|
<div class="bar-info">
|
|
1541
|
-
<span class="header-avatar"
|
|
1549
|
+
<span class="header-avatar">
|
|
1550
|
+
${this.profilePictureUrl
|
|
1551
|
+
? html`<img
|
|
1552
|
+
class="avatar-img"
|
|
1553
|
+
src="${this.profilePictureUrl}"
|
|
1554
|
+
alt="${initial}"
|
|
1555
|
+
@error=${(e: Event) => {
|
|
1556
|
+
(e.target as HTMLImageElement).style.display = "none";
|
|
1557
|
+
}}
|
|
1558
|
+
/>`
|
|
1559
|
+
: initial}
|
|
1560
|
+
</span>
|
|
1542
1561
|
<span class="bar-email"
|
|
1543
1562
|
>${this.user.email || this.user.id}</span
|
|
1544
1563
|
>
|
|
@@ -1563,7 +1582,18 @@ export class HankoAuth extends LitElement {
|
|
|
1563
1582
|
aria-haspopup="true"
|
|
1564
1583
|
class="dropdown-trigger"
|
|
1565
1584
|
>
|
|
1566
|
-
<span class="header-avatar"
|
|
1585
|
+
<span class="header-avatar">
|
|
1586
|
+
${this.profilePictureUrl
|
|
1587
|
+
? html`<img
|
|
1588
|
+
class="avatar-img"
|
|
1589
|
+
src="${this.profilePictureUrl}"
|
|
1590
|
+
alt="${initial}"
|
|
1591
|
+
@error=${(e: Event) => {
|
|
1592
|
+
(e.target as HTMLImageElement).style.display = "none";
|
|
1593
|
+
}}
|
|
1594
|
+
/>`
|
|
1595
|
+
: initial}
|
|
1596
|
+
</span>
|
|
1567
1597
|
|
|
1568
1598
|
${this.osmConnected
|
|
1569
1599
|
? html`
|