@hotosm/hanko-auth 0.5.2 → 0.5.4
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 +59 -53
- package/dist/hanko-auth.esm.js +21 -12
- package/dist/hanko-auth.iife.js +15 -15
- package/dist/hanko-auth.umd.js +15 -15
- package/package.json +8 -9
- package/src/hanko-auth.styles.ts +6 -6
- package/src/hanko-auth.ts +23 -2
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Web Component: `<hotosm-auth>`
|
|
2
2
|
|
|
3
|
-
Lit-based web component for HOTOSM SSO authentication with Hanko and
|
|
3
|
+
Lit-based web component for HOTOSM SSO authentication with Hanko and
|
|
4
|
+
OpenStreetMap integration.
|
|
4
5
|
|
|
5
6
|
## Installation
|
|
6
7
|
|
|
@@ -56,57 +57,57 @@ export function AuthButton({ hankoUrl, onLogin }) {
|
|
|
56
57
|
|
|
57
58
|
### Core
|
|
58
59
|
|
|
59
|
-
| Attribute
|
|
60
|
-
|
|
|
61
|
-
| `hanko-url` | string | `window.location.origin` | Login service URL
|
|
62
|
-
| `base-path` | string | `""`
|
|
63
|
-
| `auth-path` | string | `/api/auth/osm`
|
|
60
|
+
| Attribute | Type | Default | Description |
|
|
61
|
+
| - | - | - | - |
|
|
62
|
+
| `hanko-url` | string | `window.location.origin` | Login service URL |
|
|
63
|
+
| `base-path` | string | `""` | Base URL for OSM OAuth endpoints |
|
|
64
|
+
| `auth-path` | string | `/api/auth/osm` | OSM auth endpoints path |
|
|
64
65
|
|
|
65
66
|
### Behavior
|
|
66
67
|
|
|
67
|
-
| Attribute
|
|
68
|
-
|
|
|
69
|
-
| `osm-required`
|
|
70
|
-
| `osm-scopes`
|
|
71
|
-
| `auto-connect`
|
|
72
|
-
| `verify-session` | boolean | `false`
|
|
68
|
+
| Attribute | Type | Default | Description |
|
|
69
|
+
| - | - | - | - |
|
|
70
|
+
| `osm-required` | boolean | `false` | Require OSM connection |
|
|
71
|
+
| `osm-scopes` | string | `"read_prefs"` | OSM scopes (space-separated) |
|
|
72
|
+
| `auto-connect` | boolean | `false` | Auto redirect to OSM OAuth |
|
|
73
|
+
| `verify-session` | boolean | `false` | Verify session on return flow |
|
|
73
74
|
|
|
74
75
|
### Display
|
|
75
76
|
|
|
76
|
-
| Attribute
|
|
77
|
-
|
|
|
78
|
-
| `show-profile`
|
|
79
|
-
| `display-name`
|
|
80
|
-
| `lang`
|
|
81
|
-
| `button-variant` | string
|
|
82
|
-
| `button-color`
|
|
83
|
-
| `display`
|
|
77
|
+
| Attribute | Type | Default | Description |
|
|
78
|
+
| - | - | - | - |
|
|
79
|
+
| `show-profile` | boolean | `false` | Show full profile |
|
|
80
|
+
| `display-name` | string | `""` | Override display name |
|
|
81
|
+
| `lang` | string | `"en"` | Locale (e.g., "en", "es", "fr"), fallback "en" |
|
|
82
|
+
| `button-variant` | string | `"filled"` | `filled`, `outline`, or `plain` |
|
|
83
|
+
| `button-color` | string | `"primary"` | `primary`, `neutral`, or `danger` |
|
|
84
|
+
| `display` | string | `"default"` | `default` (avatar) or `bar` mode |
|
|
84
85
|
|
|
85
86
|
### Redirects
|
|
86
87
|
|
|
87
|
-
| Attribute
|
|
88
|
+
| Attribute | Type | Default | Description |
|
|
88
89
|
| ----------------------- | ------ | ------- | -------------------------- |
|
|
89
|
-
| `redirect-after-login`
|
|
90
|
-
| `redirect-after-logout` | string | `""`
|
|
90
|
+
| `redirect-after-login` | string | `""` | URL after successful login |
|
|
91
|
+
| `redirect-after-logout` | string | `""` | URL after logout |
|
|
91
92
|
|
|
92
93
|
### Cross-app
|
|
93
94
|
|
|
94
|
-
| Attribute
|
|
95
|
+
| Attribute | Type | Default | Description |
|
|
95
96
|
| ------------------- | ------ | ------- | ----------------------------- |
|
|
96
|
-
| `mapping-check-url` | string | `""`
|
|
97
|
-
| `app-id`
|
|
97
|
+
| `mapping-check-url` | string | `""` | URL to check user mapping |
|
|
98
|
+
| `app-id` | string | `""` | App identifier for onboarding |
|
|
98
99
|
|
|
99
100
|
## Events
|
|
100
101
|
|
|
101
102
|
The component dispatches the following custom events:
|
|
102
103
|
|
|
103
|
-
| Event
|
|
104
|
+
| Event | Detail | When |
|
|
104
105
|
| --------------- | ---------------------- | --------------------------- |
|
|
105
|
-
| `hanko-login`
|
|
106
|
-
| `osm-connected` | `{ osmData: OSMData }` | OSM account linked
|
|
107
|
-
| `osm-skipped`
|
|
108
|
-
| `auth-complete` | `{}`
|
|
109
|
-
| `logout`
|
|
106
|
+
| `hanko-login` | `{ user: HankoUser }` | User logged in |
|
|
107
|
+
| `osm-connected` | `{ osmData: OSMData }` | OSM account linked |
|
|
108
|
+
| `osm-skipped` | `{}` | User skipped OSM connection |
|
|
109
|
+
| `auth-complete` | `{}` | Auth flow complete |
|
|
110
|
+
| `logout` | `{}` | User logged out |
|
|
110
111
|
|
|
111
112
|
### Event Handling Example
|
|
112
113
|
|
|
@@ -129,9 +130,12 @@ auth.addEventListener("logout", () => {
|
|
|
129
130
|
|
|
130
131
|
## Flash Prevention (localStorage cache)
|
|
131
132
|
|
|
132
|
-
On remount (e.g. React navigation), the component checks `localStorage` for a
|
|
133
|
+
On remount (e.g. React navigation), the component checks `localStorage` for a
|
|
134
|
+
cached user under the key `hotosm-auth-user`. If found, it skips the loading
|
|
135
|
+
spinner and renders immediately with the cached user.
|
|
133
136
|
|
|
134
|
-
The component reads from this key but does not write to it. The host app is
|
|
137
|
+
The component reads from this key but does not write to it. The host app is
|
|
138
|
+
responsible for keeping it in sync:
|
|
135
139
|
|
|
136
140
|
```js
|
|
137
141
|
// Write on login
|
|
@@ -145,7 +149,8 @@ auth.addEventListener("logout", () => {
|
|
|
145
149
|
});
|
|
146
150
|
```
|
|
147
151
|
|
|
148
|
-
If the key is absent (first visit, after logout, or cleared storage), the
|
|
152
|
+
If the key is absent (first visit, after logout, or cleared storage), the
|
|
153
|
+
component falls back to its normal loading flow - no change in behavior.
|
|
149
154
|
|
|
150
155
|
## Usage Modes
|
|
151
156
|
|
|
@@ -194,7 +199,8 @@ Customize the login button appearance with `button-variant` and `button-color`:
|
|
|
194
199
|
|
|
195
200
|
### Bar Mode
|
|
196
201
|
|
|
197
|
-
Shows a full-width bar with avatar, email, and chevron arrow (ideal for mobile
|
|
202
|
+
Shows a full-width bar with avatar, email, and chevron arrow (ideal for mobile
|
|
203
|
+
drawers/menus):
|
|
198
204
|
|
|
199
205
|
```html
|
|
200
206
|
<hotosm-auth
|
|
@@ -226,26 +232,26 @@ The component uses Shadow DOM and can be customized using CSS custom properties.
|
|
|
226
232
|
|
|
227
233
|
#### Whole component
|
|
228
234
|
|
|
229
|
-
| Property
|
|
230
|
-
|
|
|
231
|
-
| `--font-family`
|
|
232
|
-
| `--font-weight`
|
|
235
|
+
| Property | Description | Default |
|
|
236
|
+
| - | - | - |
|
|
237
|
+
| `--font-family` | Font family for all text | `system-ui` |
|
|
238
|
+
| `--font-weight` | Font weight for all text | `500` |
|
|
233
239
|
|
|
234
240
|
#### Login button
|
|
235
241
|
|
|
236
|
-
| Property
|
|
237
|
-
|
|
|
238
|
-
| `--login-btn-margin`
|
|
239
|
-
| `--login-btn-padding`
|
|
240
|
-
| `--login-btn-bg-color`
|
|
241
|
-
| `--login-btn-hover-bg-color`
|
|
242
|
-
| `--login-btn-border-radius`
|
|
243
|
-
| `--login-btn-text-color`
|
|
244
|
-
| `--login-btn-text-size`
|
|
245
|
-
| `--login-btn-font-family`
|
|
246
|
-
| `--login-btn-font-weight`
|
|
247
|
-
|
|
248
|
-
|
|
242
|
+
| Property | Description | Default |
|
|
243
|
+
| - | - | - |
|
|
244
|
+
| `--login-btn-margin` | Margin around the login button | `0` |
|
|
245
|
+
| `--login-btn-padding` | Padding inside button | `x-small ...` |
|
|
246
|
+
| `--login-btn-bg-color` | Button bg color | `var(--hot-color-primary-1000)` |
|
|
247
|
+
| `--login-btn-hover-bg-color` | Hover bg | `primary-900` |
|
|
248
|
+
| `--login-btn-border-radius` | Button radius | `radius-medium` |
|
|
249
|
+
| `--login-btn-text-color` | Button text color | `white` |
|
|
250
|
+
| `--login-btn-text-size` | Button text size | `var(--hot-font-size-medium)` |
|
|
251
|
+
| `--login-btn-font-family` | Button font family | from `--font-family` |
|
|
252
|
+
| `--login-btn-font-weight` | Button font weight | from `--font-weight` |
|
|
253
|
+
|
|
254
|
+
### Example
|
|
249
255
|
|
|
250
256
|
```css
|
|
251
257
|
hotosm-auth {
|
package/dist/hanko-auth.esm.js
CHANGED
|
@@ -4018,7 +4018,7 @@ const as = () => (nn && nn.abort(), nn = new AbortController(), nn.signal), Lt =
|
|
|
4018
4018
|
|
|
4019
4019
|
.login-link {
|
|
4020
4020
|
color: var(--login-btn-text-color, white);
|
|
4021
|
-
font-size: var(--
|
|
4021
|
+
font-size: var(--hot-font-size-small);
|
|
4022
4022
|
border-radius: var(
|
|
4023
4023
|
--login-btn-border-radius,
|
|
4024
4024
|
var(--hot-border-radius-medium)
|
|
@@ -4047,11 +4047,11 @@ const as = () => (nn && nn.abort(), nn = new AbortController(), nn.signal), Lt =
|
|
|
4047
4047
|
border: none;
|
|
4048
4048
|
}
|
|
4049
4049
|
.login-link.filled.primary {
|
|
4050
|
-
background: var(--
|
|
4050
|
+
background: var(--hot-color-gray-950);
|
|
4051
4051
|
color: var(--login-btn-text-color, white);
|
|
4052
4052
|
}
|
|
4053
4053
|
.login-link.filled.primary:hover {
|
|
4054
|
-
background: var(--
|
|
4054
|
+
background: var(--hot-color-primary-800);
|
|
4055
4055
|
}
|
|
4056
4056
|
.login-link.filled.neutral {
|
|
4057
4057
|
background: var(--login-btn-bg-color, var(--hot-color-neutral-600));
|
|
@@ -4074,8 +4074,8 @@ const as = () => (nn && nn.abort(), nn = new AbortController(), nn.signal), Lt =
|
|
|
4074
4074
|
border: 1px solid;
|
|
4075
4075
|
}
|
|
4076
4076
|
.login-link.outline.primary {
|
|
4077
|
-
border-color: var(--login-btn-bg-color, var(--hot-color-primary-
|
|
4078
|
-
color: var(--login-btn-text-color, var(--hot-color-primary-
|
|
4077
|
+
border-color: var(--login-btn-bg-color, var(--hot-color-primary-950));
|
|
4078
|
+
color: var(--login-btn-text-color, var(--hot-color-primary-950));
|
|
4079
4079
|
}
|
|
4080
4080
|
.login-link.outline.primary:hover {
|
|
4081
4081
|
background: var(--login-btn-hover-bg-color, var(--hot-color-primary-50));
|
|
@@ -4101,7 +4101,7 @@ const as = () => (nn && nn.abort(), nn = new AbortController(), nn.signal), Lt =
|
|
|
4101
4101
|
border: none;
|
|
4102
4102
|
}
|
|
4103
4103
|
.login-link.plain.primary {
|
|
4104
|
-
color: var(--login-btn-text-color, var(--hot-color-primary-
|
|
4104
|
+
color: var(--login-btn-text-color, var(--hot-color-primary-950));
|
|
4105
4105
|
}
|
|
4106
4106
|
.login-link.plain.primary:hover {
|
|
4107
4107
|
background: var(--login-btn-hover-bg-color, var(--hot-color-primary-50));
|
|
@@ -4848,7 +4848,7 @@ let oe = class extends qt {
|
|
|
4848
4848
|
constructor() {
|
|
4849
4849
|
super(), this.hankoUrlAttr = "", this.basePath = "", this.authPath = "/api/auth/osm", this.osmRequired = !1, this.osmScopes = "read_prefs", this.showProfile = !1, this.redirectAfterLogin = "", this.autoConnect = !1, this.verifySession = !1, this.redirectAfterLogout = "", this.displayNameAttr = "", this.mappingCheckUrl = "", this.appId = "", this.loginUrl = "", this.lang = "en", this.buttonVariant = "plain", this.buttonColor = "primary", this.display = "default", this.user = null, this.osmConnected = !1, this.osmData = null, this.osmLoading = !1, this.loading = !0, this.error = null, this.hankoReady = !1, this.profileDisplayName = "", this.profilePictureUrl = "", this.hasAppMapping = !1, this.userProfileLanguage = null, this.isOpen = !1, this.handleOutsideClick = (n) => {
|
|
4850
4850
|
this.contains(n.target) || this.closeDropdown();
|
|
4851
|
-
}, this._debugMode = !1, this._lastSessionId = null, this._hanko = null, this._isPrimary = !1, this._hankoObserver = null, this._signUpHeadlines = /* @__PURE__ */ new Set([
|
|
4851
|
+
}, this._debugMode = !1, this._lastSessionId = null, this._hanko = null, this._isPrimary = !1, this._sessionCheckFailures = 0, this._sessionCheckBackoffTimer = null, this._hankoObserver = null, this._signUpHeadlines = /* @__PURE__ */ new Set([
|
|
4852
4852
|
"Create an account",
|
|
4853
4853
|
// en (our override)
|
|
4854
4854
|
"Crear cuenta",
|
|
@@ -4875,9 +4875,9 @@ let oe = class extends qt {
|
|
|
4875
4875
|
"Entrar"
|
|
4876
4876
|
// pt loginEmailNoSignup
|
|
4877
4877
|
]), this._handleVisibilityChange = () => {
|
|
4878
|
-
this._isPrimary && !document.hidden && !this.showProfile && !this.user && (this.log("Page visible, re-checking session..."), this.checkSession());
|
|
4878
|
+
this._isPrimary && (this._sessionCheckBackoffTimer || !document.hidden && !this.showProfile && !this.user && (this.log("Page visible, re-checking session..."), this.checkSession()));
|
|
4879
4879
|
}, this._handleWindowFocus = () => {
|
|
4880
|
-
this._isPrimary && !this.showProfile && !this.user && (this.log("Window focused, re-checking session..."), this.checkSession());
|
|
4880
|
+
this._isPrimary && (this._sessionCheckBackoffTimer || !this.showProfile && !this.user && (this.log("Window focused, re-checking session..."), this.checkSession()));
|
|
4881
4881
|
}, this._handleExternalLogin = (n) => {
|
|
4882
4882
|
var e;
|
|
4883
4883
|
if (!this._isPrimary) return;
|
|
@@ -5040,6 +5040,13 @@ let oe = class extends qt {
|
|
|
5040
5040
|
this.logError("Failed to initialize hanko-auth:", n), this.error = n.message, this.loading = !1, this._broadcastState();
|
|
5041
5041
|
}
|
|
5042
5042
|
}
|
|
5043
|
+
_scheduleSessionRetry() {
|
|
5044
|
+
if (this._sessionCheckBackoffTimer) return;
|
|
5045
|
+
const n = Math.min(1e3 * 2 ** this._sessionCheckFailures, 6e4);
|
|
5046
|
+
this.log(`Session check failed, retrying in ${n / 1e3}s (attempt ${this._sessionCheckFailures})`), this._sessionCheckBackoffTimer = setTimeout(() => {
|
|
5047
|
+
this._sessionCheckBackoffTimer = null, this.checkSession();
|
|
5048
|
+
}, n);
|
|
5049
|
+
}
|
|
5043
5050
|
async checkSession() {
|
|
5044
5051
|
var n, t, e, o, i;
|
|
5045
5052
|
if (this.log("Checking for existing Hanko session..."), !this._hanko) {
|
|
@@ -5070,7 +5077,7 @@ let oe = class extends qt {
|
|
|
5070
5077
|
));
|
|
5071
5078
|
return;
|
|
5072
5079
|
}
|
|
5073
|
-
this.log("Valid Hanko session found via cookie"), this.log("Session data:", s);
|
|
5080
|
+
this._sessionCheckFailures = 0, this.log("Valid Hanko session found via cookie"), this.log("Session data:", s);
|
|
5074
5081
|
try {
|
|
5075
5082
|
const d = await fetch(`${this.hankoUrl}/me`, {
|
|
5076
5083
|
method: "GET",
|
|
@@ -5134,10 +5141,12 @@ let oe = class extends qt {
|
|
|
5134
5141
|
} else
|
|
5135
5142
|
this.log("No valid session cookie found - user needs to login");
|
|
5136
5143
|
} catch (r) {
|
|
5137
|
-
this.log("Session validation failed:", r), this.
|
|
5144
|
+
this._sessionCheckFailures++, this.log("Session validation failed:", r), this._scheduleSessionRetry();
|
|
5145
|
+
return;
|
|
5138
5146
|
}
|
|
5139
5147
|
} catch (r) {
|
|
5140
|
-
this.log("Session check error:", r), this.
|
|
5148
|
+
this._sessionCheckFailures++, this.log("Session check error:", r), this._scheduleSessionRetry();
|
|
5149
|
+
return;
|
|
5141
5150
|
} finally {
|
|
5142
5151
|
this._isPrimary && this._broadcastState();
|
|
5143
5152
|
}
|