@gandalan/weblibs 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,42 +1,32 @@
1
1
  # WebLibs for Gandalan JS/TS/Svelte projects
2
2
 
3
- ## IDAS API mit JavaScript/TypeScript verwenden
4
-
3
+ ## Initialize Fluent IDAS API + local API
4
+ Example:
5
5
  ```js
6
- import { IDASFactory } from '@gandalan/weblibs';
7
- let idas = IDASFactory.create();
8
- ```
6
+ import { fetchEnvConfig, fluentApi, fluentIdasAuthManager } from '@gandalan/weblibs';
9
7
 
10
- IDAS ab Version 1.0.0 verwendet JWT-Token für die Authentifizierung mit WebAPI.
8
+ async function initializeAuthAndApi() {
9
+ const appToken = 'your-app-token';
10
+ const envConfig = await fetchEnvConfig('dev'); // Replace 'dev' with your desired environment
11
11
 
12
- Danach z.B. Zugriff auf die Mandant-Guid:
12
+ const authManager = await fluentIdasAuthManager(appToken, envConfig.idas).init();
13
+ if (!authManager) {
14
+ return; // init() has redirected to login.
15
+ }
13
16
 
14
- ```js
15
- let mandantGuid = await idas.then(i => i.mandantGuid);
17
+ globalThis.idas = fluentApi(envConfig.idas, authManager); // IDAS-API instance
18
+ globalThis.api = fluentApi("/api/", authManager); // Local API instance
19
+ }
16
20
  ```
17
21
 
18
- Datenzugriffe erfolgen über die Objekte innerhalb der IDAS-Klasse
22
+ ## Usage samples
19
23
 
20
24
  ```js
21
- let loader = Promise.all([
22
- idas.
23
- .then(i => i.mandanten.getAll())
24
- .then(d => mandanten = d.sort((a,b) => a.Name.localeCompare(b.Name)))
25
- .catch(e => error = e),
26
- idas
27
- .then(i => i.rollen.getAll())
28
- .then(d => rollen = d.sort((a,b) => a.Name.localeCompare(b.Name)))
29
- .catch(e => error = e)
30
- ])
31
- .then(_ => setMandant(mandantGuid));
32
- ```
33
-
34
- der hier eingeführte `loader` kann mit dem `{#await}`-Svelte-Konstrukt verwendet werden:
25
+ // Example IDAS API usage
26
+ const responseIdas = await globalThis.idas.get('mandanten');
27
+ console.log(responseIdas[0].Name);
35
28
 
36
- ```js
37
- {#await loader}
38
- <progress />
39
- {:then}
40
- ...
41
- {/await}
29
+ // Example local API usage
30
+ const responseApi = await globalThis.api.get('some-endpoint');
31
+ console.log(responseApi);
42
32
  ```
package/api/fluentApi.js CHANGED
@@ -1,157 +1,29 @@
1
- import { jwtDecode } from "jwt-decode";
2
1
  import { restClient } from "./fluentRestClient";
3
- import { authBuilder } from "./fluentAuthBuilder";
4
-
5
- /**
6
- * @typedef {Object} EnvironmentConfig
7
- * @property {string} name - The environment name.
8
- * @property {string} version - The version number.
9
- * @property {string} cms - The CMS URL.
10
- * @property {string} idas - The IDAS API URL.
11
- * @property {string} store - The store API URL.
12
- * @property {string} docs - The documentation URL.
13
- * @property {string} notify - The notification service URL.
14
- * @property {string} feedback - The feedback service URL.
15
- * @property {string} helpcenter - The help center URL.
16
- * @property {string} reports - The reports service URL.
17
- * @property {string} webhookService - The webhook service URL.
18
- */
19
-
20
- /**
21
- * buffer for environment data
22
- * @private
23
- * @type {Object.<string, EnvironmentConfig>}
24
- */
25
- const envs = {};
26
-
27
- /**
28
- * configure the time before token expiry to renew
29
- * @type {number}
30
- */
31
- const JWT_SAFE_RENEWAL = 30; // seconds before token expiry to renew
32
-
33
- /**
34
- * fetches the environment data from the hub
35
- * @export
36
- * @async
37
- * @param {string} [env="", env="dev", env="staging", env="produktiv"]
38
- * @returns {Promise<EnvironmentConfig>}
39
- */
40
- export async function fetchEnv(env = "") {
41
- if (!(env in envs)) {
42
- const hubUrl = `https://connect.idas-cloudservices.net/api/Endpoints?env=${env}`;
43
- console.log("fetching env", hubUrl);
44
- const r = await fetch(hubUrl);
45
- const data = await r.json();
46
- envs[env] = data;
47
- }
48
- return envs[env];
49
- }
50
-
51
- /**
52
- * @typedef {Object} JwtTokenExt
53
- * @property {string} id
54
- * @property {string} refreshToken
55
- */
56
-
57
- /**
58
- * decode the JWT token and return the refresh token
59
- * @export
60
- * @param {string} token
61
- * @returns {string}
62
- */
63
- export function getRefreshToken(token) {
64
- const decoded = /** @type {JwtTokenExt} */(jwtDecode(token));
65
- return decoded.refreshToken;
66
- }
67
-
68
- /**
69
- * check if the token is still valid
70
- * - checks the expiry date and the JWT_SAFE_RENEWAL buffer
71
- *
72
- * @export
73
- * @param {string} token
74
- * @returns {boolean}
75
- */
76
- export function isTokenValid(token)
77
- {
78
- try
79
- {
80
- const decoded = jwtDecode(token);
81
- if (!decoded || !decoded.exp)
82
- throw new Error("Invalid token");
83
- return (decoded.exp - JWT_SAFE_RENEWAL > Date.now() / 1000);
84
- }
85
- catch {
86
- return false;
87
- }
88
- }
89
2
 
90
3
  /**
91
4
  * @typedef {Object} FluentApi
92
5
  * @property {string} baseUrl - The base URL for API requests.
93
- * @property {string} authUrl - The authentication URL.
94
- * @property {string} env - The environment setting.
95
- * @property {string} appToken - The application token.
96
- * @property {string} storageEntry - The storage entry.
97
- * @property {string} token - The JWT token for authorization.
98
- * @property {string} refreshToken - The refresh token.
99
- * @property {function(EnvironmentConfig) : FluentApi} useEnvironment - Sets the environment and returns the FluentApi object.
100
- * @property {function(string) : FluentApi} useAppToken - Sets the application token and returns the FluentApi object.
6
+ * @property {FluentAuthManager} authManager - The authentication manager.
101
7
  * @property {function(string) : FluentApi} useBaseUrl - Sets the base URL for API requests and returns the FluentApi object.
102
- * @property {function(string) : FluentApi} useAuthUrl - Sets the authentication URL and returns the FluentApi object.
103
- * @property {function(string) : FluentApi} useToken - Sets the JWT token for authorization and returns the FluentApi object.
104
- * @property {function(string) : FluentApi} useRefreshToken - Sets the refresh token and returns the FluentApi object.
105
- * @property {function() : FluentApi} useGlobalAuth - Uses global authentication tokens and returns the FluentApi object.
8
+ * @property {function(FluentAuthManager) : FluentApi} useAuthManager - Sets the auth manager and returns the FluentApi object.
106
9
  * @property {function(string) : object|Array<any>} get - Async function to perform GET requests.
107
10
  * @property {function(string, object|null) : object|Array<any>} put - Async function to perform PUT requests with a payload.
108
11
  * @property {function(string, object|null) : object|Array<any>} post - Async function to perform POST requests with a payload.
109
12
  * @property {function(string) : object|Array<any>} delete - Async function to perform DELETE requests.
110
- * @property {Function} ensureAuthenticated - Ensures the user is authenticated before making a request.
111
- * @property {Function} ensureBaseUrlIsSet - Ensures the base URL is set before making a request.
112
- * @property {Function} redirectToLogin - Redirects to the login page.
113
13
  */
114
14
 
115
15
  /**
116
- * Builds a client to communicate with the IDAS api in a fluent syntax
117
- *
118
- * @return {FluentApi}
16
+ * Builds a client to communicate with the IDAS or local API using fluent syntax.
17
+ *
18
+ * @return {FluentApi} A configured API client instance.
119
19
  */
120
20
  export function createApi() {
121
21
  return {
22
+ authManager: {},
122
23
  baseUrl: "",
123
- authUrl: "",
124
- env: "",
125
- appToken: "",
126
- storageEntry: "",
127
- token: "",
128
- refreshToken: "",
129
-
130
- /**
131
- * set the environment to use
132
- *
133
- * @param {EnvironmentConfig} env
134
- * @return {FluentApi}
135
- */
136
- useEnvironment(env = {}) {
137
- this.env = env;
138
- this.baseUrl = env.idas;
139
- this.authUrl = env.idas;
140
- return this;
141
- },
142
-
143
- /**
144
- * set the app token to use
145
- *
146
- * @param {string} [newApptoken=""]
147
- * @return {FluentApi}
148
- */
149
- useAppToken(newApptoken = "") {
150
- this.appToken = newApptoken; return this;
151
- },
152
24
 
153
25
  /**
154
- * set the base URL for API requests
26
+ * Sets the base URL for API requests.
155
27
  *
156
28
  * @param {string} [url=""]
157
29
  * @return {FluentApi}
@@ -162,96 +34,59 @@ export function createApi() {
162
34
  },
163
35
 
164
36
  /**
165
- * set the authentication URL
166
- *
167
- * @param {string} [url=""]
168
- * @return {FluentApi}
169
- */
170
- useAuthUrl(url = "") {
171
- this.authUrl = url; return this;
172
- },
173
-
174
- /**
175
- * set the JWT token for authorization
176
- *
177
- * @param {string} [jwtToken=""]
178
- * @return {FluentApi}
179
- */
180
- useToken(jwtToken = "") {
181
- this.token = jwtToken; return this;
182
- },
183
-
184
- /**
185
- * set the refresh token
186
- *
187
- * @param {string} [storedRefreshToken=""]
188
- * @return {FluentApi}
189
- */
190
- useRefreshToken(storedRefreshToken = "") {
191
- this.refreshToken = storedRefreshToken; return this;
192
- },
193
-
194
- /**
195
- * tell the client to use the global authentication tokens
37
+ * Sets the authentication manager.
196
38
  *
39
+ * @param {FluentAuthManager} authManager
197
40
  * @return {FluentApi}
198
41
  */
199
- useGlobalAuth() {
200
- // eslint-disable-next-line no-undef
201
- this.token = globalThis.idasTokens.token;
202
- // eslint-disable-next-line no-undef
203
- this.refreshToken = globalThis.idasTokens.refreshToken;
204
- // eslint-disable-next-line no-undef
205
- this.appToken = globalThis.idasTokens.appToken;
42
+ useAuthManager(authManager) {
43
+ this.authManager = authManager;
206
44
  return this;
207
45
  },
208
-
46
+
209
47
  /**
210
- * GET request, ensure authenticated if needed
211
- *
212
- * @async
213
- * @param {string} [url=""]
214
- * @param {boolean} [auth=true]
215
- * @returns {Promise<Object>}
216
- */
48
+ * Sends a GET request, ensuring authentication if needed.
49
+ *
50
+ * @async
51
+ * @param {string} [url=""]
52
+ * @param {boolean} [auth=true]
53
+ * @returns {Promise<Object>}
54
+ */
217
55
  async get(url = "", auth = true) {
218
- if (auth)
219
- await this.ensureAuthenticated();
220
- return restClient().useBaseUrl(this.baseUrl).useToken(this.token).get(url);
56
+ await this.preCheck(auth);
57
+ return await this.createRestClient().get(url);
221
58
  },
222
59
 
223
60
  /**
224
- * PUT request, ensure authenticated if needed
225
- *
226
- * @async
227
- * @param {string} [url=""]
228
- * @param {Object} [payload={}]
229
- * @param {boolean} [auth=true]
230
- * @returns {Promise<Object>}
231
- */
61
+ * Sends a PUT request with a payload, ensuring authentication if needed.
62
+ *
63
+ * @async
64
+ * @param {string} [url=""]
65
+ * @param {Object} [payload={}]
66
+ * @param {boolean} [auth=true]
67
+ * @returns {Promise<Object>}
68
+ */
232
69
  async put(url = "", payload = {}, auth = true) {
233
- if (auth)
234
- await this.ensureAuthenticated();
235
- return restClient().useBaseUrl(this.baseUrl).useToken(this.token).put(url, payload);
70
+ await this.preCheck(auth);
71
+ return await this.createRestClient().put(url, payload);
236
72
  },
237
73
 
238
74
  /**
239
- * POST request, ensure authenticated if needed
240
- *
241
- * @async
242
- * @param {string} [url=""]
243
- * @param {Object} [payload={}]
244
- * @param {boolean} [auth=true]
245
- * @returns {Promise<Object>}
246
- */
75
+ * Sends a POST request with a payload, ensuring authentication if needed.
76
+ *
77
+ * @async
78
+ * @param {string} [url=""]
79
+ * @param {Object} [payload={}]
80
+ * @param {boolean} [auth=true]
81
+ * @returns {Promise<Object>}
82
+ */
247
83
  async post(url = "", payload = {}, auth = true) {
248
- if (auth)
249
- await this.ensureAuthenticated();
250
- return restClient().useBaseUrl(this.baseUrl).useToken(this.token).post(url, payload);
84
+ await this.preCheck(auth);
85
+ return await this.createRestClient().post(url, payload);
251
86
  },
252
-
87
+
253
88
  /**
254
- * DELETE request, ensure authenticated if needed
89
+ * Sends a DELETE request, ensuring authentication if needed.
255
90
  *
256
91
  * @async
257
92
  * @param {string} [url=""]
@@ -259,71 +94,51 @@ export function createApi() {
259
94
  * @returns {Promise<Object>}
260
95
  */
261
96
  async delete(url = "", auth = true) {
262
- if (auth)
263
- await this.ensureAuthenticated();
264
- return restClient().useBaseUrl(this.baseUrl).useToken(this.token).delete(url);
97
+ await this.preCheck(auth);
98
+ return await this.createRestClient().delete(url);
265
99
  },
266
100
 
267
101
  /**
268
- * Ensure the user is authenticated before making a request
102
+ * Creates the REST client instance with the current configuration.
269
103
  *
270
- * @async
271
104
  * @private
105
+ * @returns {FluentRestClient}
272
106
  */
273
- async ensureAuthenticated() {
274
- if (this.token && isTokenValid(this.token))
275
- return;
276
-
277
- try {
278
- const temptoken = await authBuilder()
279
- .useAppToken(this.appToken)
280
- .useBaseUrl(this.authUrl || this.env.idas)
281
- .useToken(this.token)
282
- .useRefreshToken(this.refreshToken)
283
- .authenticate() || "";
284
-
285
- if (!temptoken) {
286
- throw new Error("not authenticated");
287
- }
107
+ createRestClient() {
108
+ return restClient().useBaseUrl(this.baseUrl).useToken(this.authManager?.token);
109
+ },
288
110
 
289
- this.token = temptoken;
290
- this.refreshToken = getRefreshToken(temptoken);
291
- // eslint-disable-next-line no-undef
292
- globalThis.idasTokens.token = this.token;
293
- // eslint-disable-next-line no-undef
294
- globalThis.idasTokens.refreshToken = this.refreshToken;
295
- // eslint-disable-next-line no-undef
296
- globalThis.idasTokens.userInfo = jwtDecode(this.token);
297
- localStorage.setItem("idas-refresh-token", this.refreshToken);
298
- } catch (e) {
299
- //this.redirectToLogin();
300
- console.error("not authenticated", e);
111
+ /**
112
+ * Ensures the user is authenticated before making a request.
113
+ *
114
+ * @private
115
+ * @async
116
+ * @param {boolean} [auth=true]
117
+ * @returns {void}
118
+ */
119
+ async preCheck(auth = true) {
120
+ if (auth && this.authManager) {
121
+ await this.authManager.ensureAuthenticated();
301
122
  }
302
123
  }
303
124
  };
304
125
  }
305
126
 
306
127
  /**
307
- * Default setup for IDAS
128
+ * Sets up a client for API requests.
308
129
  *
309
- * @export
310
- * @param {string} [appToken=""]
311
- * @return {FluentApi}
312
- */
313
- export function idasApi(appToken = "") {
314
- return createApi()
315
- .useGlobalAuth()
316
- .useAppToken(appToken);
317
- }
318
-
319
- /**
320
- * Default setup for local API
130
+ * - Requests will be sent to the url provided.
131
+ * - Example usage:
132
+ * const api = fluentApi("https://jsonplaceholder.typicode.com/todos/", null);
133
+ * api.get("1"); // Sends a GET request to https://jsonplaceholder.typicode.com/todos/1.
321
134
  *
322
135
  * @export
323
- * @return {FluentApi}
136
+ * @param {string} url - The base URL for API requests.
137
+ * @param {FluentAuthManager} authManager - The authentication manager instance.
138
+ * @return {FluentApi} Configured API instance for local use.
324
139
  */
325
- export function localApi() {
140
+ export function fluentApi(url, authManager) {
326
141
  return createApi()
327
- .useGlobalAuth()
328
- .useBaseUrl("api");
142
+ .useAuthManager(authManager)
143
+ .useBaseUrl(url);
329
144
  }
@@ -0,0 +1,291 @@
1
+ import { jwtDecode } from "jwt-decode";
2
+ import validator from "validator";
3
+ import { popRefreshTokenFromUrl } from "./fluentAuthUtils";
4
+
5
+ /**
6
+ * @typedef {Object} FluentAuthManager
7
+ * @property {string} appToken - The application token.
8
+ * @property {string} authUrl - The authentication URL.
9
+ * @property {string} token - The JWT token for authorization.
10
+ * @property {string} refreshToken - The refresh token.
11
+ * @property {object} userInfo - The user information.
12
+ * @property {function(string) : FluentAuthManager} useAppToken - Sets the application token and returns the FluentApi object.
13
+ * @property {function(string) : FluentAuthManager} useBaseUrl - Sets the base URL for authentication and returns the FluentApi object.
14
+ * @property {function(string|null) : FluentAuthManager} useToken - Sets the JWT token and returns the FluentApi object. Only intended for usage with Service Tokens.
15
+ * @property {function(string|null) : FluentAuthManager} useRefreshToken - Sets the refresh token and returns the FluentApi object.
16
+ * @property {function} ensureAuthenticated - Ensures the user is authenticated before making a request.
17
+ * @property {function() : string} authenticate - Authenticates the user with username and password, or refreshes the token.
18
+ * @property {function():Promise<FluentAuthManager|null>} init - Returns promise for authManager. Returns null if not authenticated.
19
+ * @property {function(string,string) : string} login - Logs in with the provided credentials.
20
+ * @property {function} tryRefreshToken - Attempts to refresh the authentication token using the refresh token.
21
+ * @property {function} updateUserSession - Updates the user session with the new token.
22
+ * @property {function} redirectToLogin - Redirects to the login page.
23
+ */
24
+
25
+ /**
26
+ * Creates a new FluentAuthManager
27
+ *
28
+ * @export
29
+ * @returns {FluentAuthManager}
30
+ */
31
+ export function createAuthManager() {
32
+ return {
33
+ appToken: "",
34
+ authUrl: "",
35
+ token: "",
36
+ refreshToken: "",
37
+ userInfo: {},
38
+
39
+ /**
40
+ * app token to use for authentication
41
+ *
42
+ * @param {string} [appToken=""]
43
+ * @returns {FluentAuthManager}
44
+ */
45
+ useAppToken(appToken = "") {
46
+ if (!validator.isUUID(appToken)) {
47
+ console.error("AppToken is not valid GUID");
48
+ return null;
49
+ }
50
+ this.appToken = appToken;
51
+ return this;
52
+ },
53
+
54
+ /**
55
+ * set the authentication URL
56
+ *
57
+ * @param {string} [url=""]
58
+ * @return {FluentAuthManager}
59
+ */
60
+ useBaseUrl(url = "") {
61
+ this.authUrl = url;
62
+ return this;
63
+ },
64
+
65
+ /**
66
+ * set the JWT token for authorization
67
+ *
68
+ * @param {string} [jwtToken=""]
69
+ * @return {FluentAuthManager}
70
+ */
71
+ useToken(jwtToken = "") {
72
+ this.token = jwtToken;
73
+ return this;
74
+ },
75
+
76
+ /**
77
+ * set the refresh token
78
+ *
79
+ * @param {string} [storedRefreshToken=""]
80
+ * @return {FluentAuthManager}
81
+ */
82
+ useRefreshToken(storedRefreshToken = "") {
83
+ this.refreshToken = storedRefreshToken;
84
+ return this;
85
+ },
86
+
87
+ /**
88
+ * Ensure the user is authenticated before making a request
89
+ *
90
+ * @async
91
+ * @private
92
+ */
93
+ async ensureAuthenticated() {
94
+ if (this.token && isTokenValid(this.token))
95
+ return;
96
+
97
+ try {
98
+ await this.authenticate();
99
+ } catch (e) {
100
+ // no redirect to login, because we're in a request
101
+ console.error("not authenticated", e);
102
+ }
103
+ },
104
+
105
+ /**
106
+ * Authenticates the user with the JWT token or refreshes the token with
107
+ * the refreshToken set before.
108
+ *
109
+ * @throws {Error} if JWT token and refreshToken are not set or both are invalid
110
+ * @return {string} the JWT token
111
+ */
112
+ async authenticate() { // benutzt bei existierendem JWT oder RefreshToken, wenn keins vorhanden ERROR
113
+ console.log("authenticating:", this.token ? `token set, exp: ${jwtDecode(this.token).exp - (Date.now() / 1000)}` : "no token,", this.refreshToken, this.appToken);
114
+
115
+ if (this.token && isTokenValid(this.token))
116
+ return;
117
+
118
+ if (this.token && !this.refreshToken)
119
+ this.refreshToken = getRefreshToken(this.token);
120
+
121
+ if (!this.refreshToken) {
122
+ throw new Error("not authenticated");
123
+ }
124
+
125
+ try {
126
+ const temptoken = await this.tryRefreshToken(this.refreshToken);
127
+ this.updateUserSession(temptoken);
128
+ } catch {
129
+ // if refresh failed
130
+ // - current token should still be valid for a while
131
+ // - or user has invalid (refresh) token and needs to login/refresh manually
132
+ }
133
+ },
134
+
135
+ /**
136
+ * Initializes the authentication object. Before calling, set the token and refresh token if available.
137
+ * If the token is not set, the refresh token will be used to try to refresh the token.
138
+ * If the token is not valid, the user will be redirected to the login page.
139
+ * If tokens are valid, they will be stored in this instance of the FluentAuthManager.
140
+ *
141
+ * Side effect if refreshToken is not set: tries to get the refreshToken from the URL or localStorage.
142
+ *
143
+ * @async
144
+ * @return {Promise<FluentAuthManager> | null} the FluentAuthManager or null if not authenticated
145
+ */
146
+ async init() {
147
+ if (!this.refreshToken) {
148
+ this.refreshToken = popRefreshTokenFromUrl() || localStorage.getItem("idas-refresh-token");
149
+ }
150
+
151
+ if (!this.token && this.refreshToken) {
152
+ this.token = await this.tryRefreshToken(this.refreshToken);
153
+ }
154
+
155
+ if (this.token && isTokenValid(this.token)) {
156
+ this.updateUserSession(this.token);
157
+ return this;
158
+ }
159
+
160
+ if (!isTokenValid(this.token)) {
161
+ this.redirectToLogin();
162
+ return null;
163
+ }
164
+ return this;
165
+ },
166
+
167
+ /**
168
+ * Login with credentials and return the JWT token
169
+ * @param {string} username
170
+ * @param {string} password
171
+ * @return {string} the JWT token
172
+ */
173
+ async login(username = "", password = "") {
174
+ if (username && password) {
175
+ const payload = { "Email": username, "Password": password, "AppToken": this.appToken };
176
+ const res = await fetch(`${this.authUrl}/LoginJwt`,
177
+ { method: "POST", body: JSON.stringify(payload), headers: { "Content-Type": "application/json" } });
178
+ this.updateUserSession((await res.json()));
179
+ return;
180
+ }
181
+ throw new Error("not authenticated");
182
+ },
183
+
184
+ /**
185
+ * try to refresh the JWT token by using the refreshToken
186
+ * @async
187
+ * @private
188
+ * @param {string} [refreshToken=""]
189
+ * @returns {unknown}
190
+ */
191
+ async tryRefreshToken(refreshToken = "") {
192
+ const payload = { "Token": refreshToken };
193
+ const res = await fetch(`${this.authUrl}LoginJwt/Refresh`,
194
+ {
195
+ method: "PUT",
196
+ body: JSON.stringify(payload),
197
+ headers: { "Content-Type": "application/json" },
198
+ });
199
+ return res.ok ? await res.json() : null;
200
+ },
201
+
202
+ /**
203
+ * update the user session with the new token
204
+ * @private
205
+ * @param {string} token
206
+ * @returns {void}
207
+ */
208
+ updateUserSession(token) {
209
+ if (token) {
210
+ this.token = token;
211
+ this.refreshToken = getRefreshToken(token);
212
+ this.userInfo = jwtDecode(this.token);
213
+ localStorage.setItem("idas-refresh-token", this.refreshToken);
214
+ }
215
+ },
216
+
217
+ /**
218
+ * Redirect to the login page
219
+ * @private
220
+ */
221
+ redirectToLogin() {
222
+ if (!window) {
223
+ return;
224
+ }
225
+
226
+ const redirectAfterAuth = new URL(window.location.href).origin;
227
+ let redirectUrl = `${redirectAfterAuth}?t=%token%`;
228
+ const url = new URL(this.authUrl);
229
+ url.pathname = "/Session";
230
+ url.search = `?a=${this.appToken}&r=${encodeURIComponent(redirectUrl)}`;
231
+ let loginUrl = url.toString();
232
+ window.location.href = loginUrl;
233
+ }
234
+ };
235
+ }
236
+
237
+ /**
238
+ * configure the time before token expiry to renew
239
+ * @type {number}
240
+ */
241
+ const JWT_SAFE_RENEWAL = 30; // seconds before token expiry to renew
242
+
243
+ /**
244
+ * @typedef {Object} JwtTokenExt
245
+ * @property {string} id
246
+ * @property {string} refreshToken
247
+ */
248
+
249
+ /**
250
+ * decode the JWT token and return the refresh token
251
+ * @export
252
+ * @param {string} token
253
+ * @returns {string}
254
+ */
255
+ export function getRefreshToken(token) {
256
+ const decoded = /** @type {JwtTokenExt} */(jwtDecode(token));
257
+ return decoded.refreshToken;
258
+ }
259
+
260
+ /**
261
+ * check if the token is still valid
262
+ * - checks the expiry date and the JWT_SAFE_RENEWAL buffer
263
+ *
264
+ * @export
265
+ * @param {string} token
266
+ * @returns {boolean}
267
+ */
268
+ export function isTokenValid(token) {
269
+ try {
270
+ const decoded = jwtDecode(token);
271
+ if (!decoded || !decoded.exp)
272
+ throw new Error("Invalid token");
273
+ return (decoded.exp - JWT_SAFE_RENEWAL > Date.now() / 1000);
274
+ }
275
+ catch {
276
+ return false;
277
+ }
278
+ }
279
+
280
+ /**
281
+ * create a new FluentAuthManager with the provided tokens
282
+ * @export
283
+ * @param {string} appToken
284
+ * @param {string} authBaseUrl
285
+ * @returns {FluentAuthManager}
286
+ */
287
+ export function fluentIdasAuthManager(appToken, authBaseUrl) {
288
+ return createAuthManager()
289
+ .useAppToken(appToken)
290
+ .useBaseUrl(authBaseUrl)
291
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Gets idas refresh token from the URL if it exists and removes it from the URL.
3
+ *
4
+ * @returns {string|null} The refresh token or null if it does not exist.
5
+ */
6
+ export function popRefreshTokenFromUrl() {
7
+ const url = new URL(window.location.href);
8
+ const refreshToken = url.searchParams.get("t");
9
+ if (refreshToken) {
10
+ url.searchParams.delete("t");
11
+ window.history.replaceState({}, document.title, url);
12
+ return refreshToken;
13
+ }
14
+ return null;
15
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @typedef {Object} EnvironmentConfig
3
+ * @property {string} name - The environment name.
4
+ * @property {string} version - The version number.
5
+ * @property {string} cms - The CMS URL.
6
+ * @property {string} idas - The IDAS API URL.
7
+ * @property {string} store - The store API URL.
8
+ * @property {string} docs - The documentation URL.
9
+ * @property {string} notify - The notification service URL.
10
+ * @property {string} feedback - The feedback service URL.
11
+ * @property {string} helpcenter - The help center URL.
12
+ * @property {string} reports - The reports service URL.
13
+ * @property {string} webhookService - The webhook service URL.
14
+ */
15
+
16
+ /**
17
+ * buffer for environment data
18
+ * @private
19
+ * @type {Object.<string, EnvironmentConfig>}
20
+ */
21
+ const envConfigs = {};
22
+
23
+ /**
24
+ * Fetches the environment data from the hub.
25
+ *
26
+ * @export
27
+ * @async
28
+ * @param {string} [envConfig=""] - The environment name (e.g., "dev", "staging", "produktiv").
29
+ * @returns {Promise<EnvironmentConfig>}
30
+ */
31
+ export async function fetchEnvConfig(envConfig = "") {
32
+ if (!(envConfig in envConfigs)) {
33
+ const hubUrl = `https://connect.idas-cloudservices.net/api/Endpoints?env=${envConfig}`;
34
+ console.log("fetching env", hubUrl);
35
+ const r = await fetch(hubUrl);
36
+ const data = await r.json();
37
+ envConfigs[envConfig] = data;
38
+ }
39
+ return envConfigs[envConfig];
40
+ }
package/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { IDASFactory } from "./api/IDAS";
2
2
  import { RESTClient } from "./api/RESTClient";
3
3
  import { initIDAS } from "./api/authUtils";
4
- export { IDASFactory, RESTClient, initIDAS };
4
+ export { IDASFactory, initIDAS, RESTClient };
5
5
 
6
- export { createApi as api, idasApi, fetchEnv, getRefreshToken } from "./api/fluentApi";
7
- export { authBuilder } from "./api/fluentAuthBuilder";
6
+ export { createApi, fluentApi } from "./api/fluentApi";
7
+ export { createAuthManager, fluentIdasAuthManager } from "./api/fluentAuthManager";
8
+ export { fetchEnvConfig } from "./api/fluentEnvUtils";
8
9
  export { restClient } from "./api/fluentRestClient";
10
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gandalan/weblibs",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "WebLibs for Gandalan JS/TS/Svelte projects",
5
5
  "keywords": [
6
6
  "gandalan"
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@mdi/js": "^7.4.47",
25
- "axios": "^1.7.7",
25
+ "axios": "^1.7.8",
26
26
  "jwt-decode": "^4.0.0",
27
27
  "validator": "^13.12.0"
28
28
  },
@@ -1,203 +0,0 @@
1
- import { jwtDecode } from "jwt-decode";
2
- import { isTokenValid, getRefreshToken } from "./fluentApi";
3
- import validator from "validator";
4
-
5
- /**
6
- * @typedef {Object} FluentAuth
7
- * @property {string} authUrl - The authentication URL.
8
- * @property {string} appToken - The application token.
9
- * @property {string} token - The JWT token.
10
- * @property {string} refreshToken - The refresh token.
11
- * @property {function(string) : FluentAuth} useAppToken - Sets the application token and returns the FluentApi object.
12
- * @property {function(string) : FluentAuth} useBaseUrl - Sets the base URL for authentication and returns the FluentApi object.
13
- * @property {function(string|null) : FluentAuth} useToken - Sets the JWT token and returns the FluentApi object.
14
- * @property {function(string|null) : FluentAuth} useRefreshToken - Sets the refresh token and returns the FluentApi object.
15
- * @property {function() : string} authenticate - Authenticates the user with username and password, or refreshes the token.
16
- * @property {Function} tryRefreshToken - Attempts to refresh the authentication token using the refresh token.
17
- * @property {Function} redirectToLogin - Redirects to the login page.
18
- * @property {Function} init - Initializes the authentication object.
19
- * @property {function(string,string) : string} login - Logs in with the provided credentials.
20
- */
21
-
22
- /**
23
- * Builds an authentication object
24
- *
25
- * @export
26
- * @returns {FluentAuth}
27
- */
28
- export function authBuilder() {
29
- return {
30
- authUrl: "",
31
- appToken: "",
32
- token: "",
33
- refreshToken: "",
34
-
35
- /**
36
- * app token to use for authentication
37
- *
38
- * @param {string} [appToken=""]
39
- * @returns {FluentAuth}
40
- */
41
- useAppToken(appToken = "") {
42
- if (!validator.isUUID(appToken)) {
43
- console.error("AppToken is not valid GUID");
44
- return null;
45
- }
46
-
47
- this.appToken = appToken; return this;
48
- },
49
-
50
- /**
51
- * base URL for authentication
52
- *
53
- * @param {string} [authUrl=""]
54
- * @returns {FluentAuth}
55
- */
56
- useBaseUrl(authUrl = "") {
57
- this.authUrl = authUrl; return this;
58
- },
59
-
60
- /**
61
- * token to use for authentication
62
- *
63
- * @param {string} [jwtToken=""]
64
- * @returns {FluentAuth}
65
- */
66
- useToken(jwtToken = "") {
67
- this.token = jwtToken; return this;
68
- },
69
-
70
- /**
71
- * sets the refresh token to use
72
- * @param {string} a refresh token (UUID v4 format)
73
- * @returns {FluentAuth}
74
- */
75
- useRefreshToken(storedRefreshToken = "") {
76
- this.refreshToken = storedRefreshToken; return this;
77
- },
78
-
79
- /**
80
- * Authenticates the user with the JWT token or refreshes the token with
81
- * the refreshToken set before
82
- * @return {string} the JWT token
83
- */
84
- async authenticate() {
85
- console.log("authenticating:", this.token ? `token set, exp: ${jwtDecode(this.token).exp - (Date.now() / 1000)}` : "no token,", this.refreshToken, this.appToken);
86
-
87
- if (this.token && isTokenValid(this.token))
88
- return this.token;
89
-
90
- if (this.token && !this.refreshToken)
91
- this.refreshToken = getRefreshToken(this.token);
92
-
93
- if (this.refreshToken) {
94
- try {
95
- const temptoken = await this.tryRefreshToken(this.refreshToken);
96
- if (temptoken) {
97
- this.token = temptoken;
98
- this.refreshToken = getRefreshToken(temptoken);
99
- }
100
- } catch {
101
- // if refresh failed, we'll return the current token
102
- // - should still be valid for a while
103
- }
104
- return this.token;
105
- }
106
-
107
- throw new Error("not authenticated");
108
- },
109
-
110
- /**
111
- * Login with credentials and return the JWT token
112
- * @param {string} username
113
- * @param {string} password
114
- * @return {string} the JWT token
115
- */
116
- async login(username = "", password = "")
117
- {
118
- if (username && password) {
119
- const payload = { "Email": username, "Password": password, "AppToken": this.appToken };
120
- const res = await fetch(`${this.authUrl}/LoginJwt`,
121
- { method: "POST", body: JSON.stringify(payload), headers: { "Content-Type": "application/json" } });
122
- const temptoken = await res.json();
123
- if (temptoken) {
124
- this.token = temptoken;
125
- this.refreshToken = getRefreshToken(temptoken);
126
- return this.token;
127
- }
128
- }
129
- throw new Error("not authenticated");
130
- },
131
-
132
- /**
133
- * try to refresh the JWT token by using the refreshToken
134
- * @async
135
- * @private
136
- * @param {string} [refreshToken=""]
137
- * @returns {unknown}
138
- */
139
- async tryRefreshToken(refreshToken = "") {
140
- const payload = { "Token": refreshToken };
141
- const res = await fetch(`${this.authUrl}LoginJwt/Refresh`,
142
- {
143
- method: "PUT",
144
- body: JSON.stringify(payload),
145
- headers: { "Content-Type": "application/json" },
146
- });
147
- return res.ok ? await res.json() : null;
148
- },
149
-
150
- /**
151
- * Initializes the authentication object. Before calling, set the token and refresh token if available.
152
- * If the token is not set, the refresh token will be used to try to refresh the token.
153
- * If the token is not valid, the user will be redirected to the login page.
154
- * If tokens are valid, they will be stored in the global variable idasTokens.
155
- */
156
- async init()
157
- {
158
- if (!this.token && this.refreshToken)
159
- {
160
- this.token = await this.tryRefreshToken(this.refreshToken);
161
- }
162
-
163
- if (this.token && isTokenValid(this.token))
164
- {
165
- this.refreshToken = getRefreshToken(this.token);
166
- localStorage.setItem("idas-refresh-token", this.refreshToken);
167
- }
168
-
169
- if (!isTokenValid(this.token))
170
- {
171
- this.redirectToLogin();
172
- }
173
-
174
- let userInfo = {};
175
- if (this.token)
176
- {
177
- userInfo = jwtDecode(this.token);
178
- }
179
- // eslint-disable-next-line no-undef
180
- globalThis.idasTokens = { token: this.token, refreshToken: this.refreshToken, appToken: this.appToken, userInfo };
181
- },
182
-
183
- /**
184
- * Redirect to the login page
185
- * @private
186
- */
187
- redirectToLogin() {
188
- if (!window) {
189
- return;
190
- }
191
-
192
- const redirectAfterAuth = new URL(window.location.href).origin;
193
- //let redirectUrl = `${redirectAfterAuth}?r=%target%&j=%jwt%&m=%mandant%`;
194
- //redirectUrl = redirectUrl.replace("%target%", encodeURIComponent(window.location.href));
195
- let redirectUrl = `${redirectAfterAuth}?t=%token%`;
196
- const url = new URL(this.authUrl);
197
- url.pathname = "/Session";
198
- url.search = `?a=${this.appToken}&r=${encodeURIComponent(redirectUrl)}`;
199
- let loginUrl = url.toString();
200
- window.location.href = loginUrl;
201
- }
202
- };
203
- }