@communecter/cocolight-api-client 1.0.10 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@communecter/cocolight-api-client",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "Client Axios simplifié pour l'API cocolight",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,8 +10,11 @@
10
10
  "main": "./dist/cocolight-api-client.cjs",
11
11
  "browser": "./dist/cocolight-api-client.browser.js",
12
12
  "exports": {
13
- "import": "./dist/cocolight-api-client.mjs.js",
14
- "require": "./dist/cocolight-api-client.cjs"
13
+ ".": {
14
+ "import": "./dist/cocolight-api-client.mjs.js",
15
+ "require": "./dist/cocolight-api-client.cjs"
16
+ },
17
+ "./mjs": "./dist/cocolight-api-client.mjs.js"
15
18
  },
16
19
  "type": "module",
17
20
  "scripts": {
@@ -33,7 +36,8 @@
33
36
  "generate:reponses": "node ./scripts/generate-constant-response-200.js",
34
37
  "generate:methodeapi": "node ./scripts/generate-methode-api.js",
35
38
  "generate:ajv-standalone": "node ./scripts/generate-validate-function-ajv.js",
36
- "generate:entities": "node scripts/generate-entities.js"
39
+ "generate:entities": "node scripts/generate-entities.js",
40
+ "generate:all-properties-schema": "node scripts/generate-all-properties-schema.js"
37
41
  },
38
42
  "keywords": [
39
43
  "communecter",
package/src/Api.js CHANGED
@@ -5,7 +5,7 @@ import { Organization } from "./api/Organization.js";
5
5
  import { Project } from "./api/Project.js";
6
6
  import { User } from "./api/User.js";
7
7
  import { UserApi } from "./api/UserApi.js";
8
- import { ApiAuthenticationError, ApiClientError } from "./error.js";
8
+ import { ApiAuthenticationError, ApiClientError, ApiError } from "./error.js";
9
9
 
10
10
  export default class Api {
11
11
  /**
@@ -51,8 +51,27 @@ export default class Api {
51
51
  */
52
52
  static async userApiLogin(userApi, email, password) {
53
53
  try {
54
- const loggedUser = await userApi.login(email, password);
55
- return new Api(loggedUser, userApi.client);
54
+
55
+ if (!userApi) {
56
+ throw new ApiError("userApi is not defined");
57
+ }
58
+ if(!userApi.client) {
59
+ throw new ApiError("userApi.client is not defined");
60
+ }
61
+
62
+ if(userApi.client.isConnected) {
63
+ // Si l'utilisateur est déjà connecté, on le récupère
64
+ // et on ne fait pas de login
65
+ const loggedUser = await userApi.meIsconnected();
66
+ return new Api(loggedUser, userApi.client);
67
+ } else {
68
+ if (!email || !password) {
69
+ throw new ApiError("email and password are required");
70
+ }
71
+ const loggedUser = await userApi.login(email, password);
72
+ return new Api(loggedUser, userApi.client);
73
+ }
74
+
56
75
  } catch (error) {
57
76
  if(error instanceof ApiClientError) {
58
77
  if(error?.details?.error) {
@@ -170,5 +189,11 @@ export default class Api {
170
189
  get endpointApi() {
171
190
  return new EndpointApi(this._client);
172
191
  }
192
+
193
+ logout () {
194
+ this.loggedUser = null;
195
+ this._client.resetSession();
196
+ this._client._logger.info("UserApi: User logged out");
197
+ }
173
198
 
174
199
  }
package/src/ApiClient.js CHANGED
@@ -12,6 +12,7 @@ import pino from "pino";
12
12
  import MongoID from "./EJSONType.js";
13
13
  import endpointsJson from "./endpoints.module.js";
14
14
  import { ApiClientError, ApiResponseError, ApiValidationError, CircuitBreakerError } from "./error.js";
15
+ import { MemoryStorageStrategy } from "./utils/TokenStorage.js";
15
16
 
16
17
  EJSON.addType("oid", value => {
17
18
  return new MongoID.ObjectID(value);
@@ -31,6 +32,7 @@ export default class ApiClient extends EventEmitter {
31
32
  * @param {number} [options.circuitBreakerThreshold=5] - Nb d'erreurs avant de bloquer
32
33
  * @param {number} [options.circuitBreakerResetTime=60000] - Ms avant de reset le breaker
33
34
  * @param {boolean} [options.fromJSONValue=true] - Si true, les données sont transformées en EJSON
35
+ * @param {TokenStorageStrategy} [options.tokenStorageStrategy=null] - Stratégie de stockage des tokens
34
36
  */
35
37
  constructor({
36
38
  baseURL,
@@ -43,7 +45,8 @@ export default class ApiClient extends EventEmitter {
43
45
  maxRetries = 0,
44
46
  circuitBreakerThreshold = 5,
45
47
  circuitBreakerResetTime = 60000,
46
- fromJSONValue = true
48
+ fromJSONValue = true,
49
+ tokenStorageStrategy = null
47
50
  } = {}) {
48
51
  super(); // EventEmitter
49
52
 
@@ -54,7 +57,6 @@ export default class ApiClient extends EventEmitter {
54
57
  this.__entityTag = "ApiClient";
55
58
 
56
59
  this._baseURL = baseURL;
57
- this._refreshToken = refreshToken;
58
60
  this._refreshUrl = refreshUrl;
59
61
  this._endpoints = endpoints;
60
62
  this._debug = debug;
@@ -122,30 +124,73 @@ export default class ApiClient extends EventEmitter {
122
124
  this._breakerOpen = false;
123
125
  this._lastBreakerOpenTime = null;
124
126
 
125
- // Applique un token initial
127
+ this._accessToken = null;
128
+ this._refreshToken = null;
129
+
130
+ // Applique un token initial s'il est fourni
131
+ if(refreshToken){
132
+ this.setRefreshToken(refreshToken);
133
+ }
134
+
126
135
  if (accessToken) {
127
136
  this.setToken(accessToken);
128
137
  }
129
138
 
139
+ this._tokenStorage = tokenStorageStrategy || new MemoryStorageStrategy();
140
+
141
+ const initialAccessToken = this._tokenStorage.getAccessToken();
142
+ if (initialAccessToken) {
143
+ this.setToken(initialAccessToken);
144
+ }
145
+
146
+ const initialRefreshToken = this._tokenStorage.getRefreshToken();
147
+ if (initialRefreshToken) {
148
+ this.setRefreshToken(initialRefreshToken);
149
+ }
150
+
151
+
130
152
  // Intercepteur 401 -> refresh
131
153
  this._client.interceptors.response.use(
132
- (response) => response,
133
- async (error) => {
134
- if (error.response && error.response.status === 401) {
135
- if (this._refreshToken) {
136
- try {
137
- const refreshed = await this._refreshAccessToken();
138
- if (refreshed) {
139
- return this._client.request(error.config);
140
- }
141
- } catch (err) {
142
- throw new ApiClientError(err.message, 401, err);
154
+ response => response,
155
+ async error => {
156
+ const originalRequest = error.config;
157
+
158
+ // Si la requête est déjà réessayée, abandonne
159
+ if (originalRequest._retry) {
160
+ this._logger.error("[ApiClient] Requête déjà retentée, échec définitif.");
161
+ throw error;
162
+ }
163
+
164
+ if (error.response && error.response.status === 401 && this._refreshToken) {
165
+ originalRequest._retry = true;
166
+
167
+ try {
168
+ this._logger.info("[ApiClient] Tentative de refresh du token...");
169
+ const refreshed = await this._refreshAccessToken();
170
+
171
+ if (refreshed) {
172
+ this._logger.info("[ApiClient] Token rafraîchi avec succès.");
173
+
174
+ // 🔑 Mise à jour EXPLICITE du header Authorization dans la requête originale
175
+ originalRequest.headers["Authorization"] = "Bearer " + this.getToken();
176
+
177
+ this._logger.info("[ApiClient] Retente la requête originale avec le nouveau token.");
178
+ return this._client.request(originalRequest);
179
+ } else {
180
+ this.resetSession();
181
+ throw new ApiClientError("Impossible de rafraîchir le token.", 401);
143
182
  }
183
+ } catch (err) {
184
+ this.resetSession();
185
+ throw new ApiClientError("Erreur lors du rafraîchissement du token.", 401, err);
144
186
  }
145
187
  }
188
+
146
189
  throw error;
147
190
  }
148
191
  );
192
+
193
+
149
194
  }
150
195
 
151
196
  /**
@@ -155,6 +200,7 @@ export default class ApiClient extends EventEmitter {
155
200
  */
156
201
  setToken(token) {
157
202
  this._accessToken = token;
203
+ this._tokenStorage.setAccessToken(token);
158
204
  this._client.defaults.headers.common["Authorization"] = "Bearer " + token;
159
205
  // Extrait l'id depuis le token et le stocke si disponible
160
206
  const userId = this._getIdFromToken(token);
@@ -177,17 +223,20 @@ export default class ApiClient extends EventEmitter {
177
223
  /**
178
224
  * Sets the refresh token for the API client.
179
225
  *
180
- * @param {string} rt - The refresh token to be set.
226
+ * @param {string} refreshToken - The refresh token to be set.
181
227
  */
182
- setRefreshToken(rt) {
183
- this._refreshToken = rt;
184
- // Vous pouvez faire de même ici si besoin
185
- const userId = this._getIdFromToken(rt);
186
- if (userId) {
187
- this._setUserId(userId);
188
- this._logger.debug(`[ApiClient] userId extrait depuis refreshToken : ${userId}`);
228
+ setRefreshToken(token) {
229
+ this._refreshToken = token;
230
+ this._tokenStorage.setRefreshToken(token);
231
+ if(this.userId === null){
232
+ // Extrait l'id depuis le token et le stocke si disponible
233
+ const userId = this._getIdFromToken(token);
234
+ if (userId) {
235
+ this._setUserId(userId);
236
+ this._logger.debug(`[ApiClient] userId extrait depuis refreshToken : ${userId}`);
237
+ }
189
238
  }
190
- this._logger.debug(`[ApiClient] setRefreshToken: ${rt}`);
239
+ this._logger.debug(`[ApiClient] setRefreshToken: ${token}`);
191
240
  }
192
241
 
193
242
  /**
@@ -235,14 +284,19 @@ export default class ApiClient extends EventEmitter {
235
284
  */
236
285
  async _refreshAccessToken() {
237
286
  if (!this._refreshToken) return false;
287
+
288
+ const refreshClient = axios.create({
289
+ baseURL: this._baseURL,
290
+ timeout: 10000, // ajuster si nécessaire
291
+ headers: { "Content-Type": "application/json" }
292
+ });
293
+
238
294
  try {
239
- const response = await this._client.post(
240
- this._refreshUrl,
241
- { refreshToken: this._refreshToken },
242
- { headers: { "Content-Type": "application/json" } }
243
- );
244
- if (response.data && response.data.accessToken) {
245
- this.setToken(response.data.accessToken);
295
+ const response = await refreshClient.post(this._refreshUrl, {
296
+ refreshToken: this._refreshToken
297
+ });
298
+ if (response.data && response.data.token) {
299
+ this.setToken(response.data.token);
246
300
  if (response.data.refreshToken) {
247
301
  this.setRefreshToken(response.data.refreshToken);
248
302
  }
@@ -775,6 +829,7 @@ export default class ApiClient extends EventEmitter {
775
829
  this.setToken(null);
776
830
  this.setRefreshToken(null);
777
831
  this._setUserId(null);
832
+ this._tokenStorage.clear();
778
833
 
779
834
  // Suppression des en-têtes
780
835
  delete this._client.defaults.headers.common["Authorization"];
@@ -1,6 +1,6 @@
1
1
  // UserApi.js
2
2
  import ApiClient from "../ApiClient.js";
3
- import { ApiResponseError } from "../error.js";
3
+ import { ApiError, ApiResponseError } from "../error.js";
4
4
  import EndpointApi from "./EndpointApi.js";
5
5
  import { News } from "./News.js";
6
6
  import { Organization } from "./Organization.js";
@@ -11,8 +11,17 @@ export class UserApi {
11
11
  constructor(options) {
12
12
  // Injection de dépendance : ApiClient est créé à partir des options
13
13
  this.client = new ApiClient(options);
14
+ // si l'option "tokenStorageStrategy" est définie, on l'utilise pour créer une instance de ApiClient
14
15
  this.loggedUser = null;
15
16
  }
17
+
18
+ get isConnected() {
19
+ return this.client.isConnected;
20
+ }
21
+ get userId() {
22
+ return this.client.userId;
23
+ }
24
+
16
25
  // Méthode d'authentification : récupère les données utilisateur depuis un endpoint
17
26
  async login(email, password) {
18
27
  return this.client.safeCall(async () => {
@@ -24,6 +33,16 @@ export class UserApi {
24
33
  });
25
34
  }
26
35
 
36
+ async meIsconnected() {
37
+ if(!this.client.isConnected || !this.client.userId) {
38
+ throw new ApiError("User not connected", 401);
39
+ }
40
+ this.client._logger.info("UserApi", "meIsconnected", this.client.userId);
41
+ this.loggedUser = new User(this.client, { id: this.client.userId }, { EndpointApi, Organization, Project, News });
42
+ return this.loggedUser;
43
+ }
44
+
45
+
27
46
  async register({
28
47
  name,
29
48
  username,
@@ -49,4 +68,5 @@ export class UserApi {
49
68
  return response.data;
50
69
  });
51
70
  }
71
+
52
72
  }
package/src/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import Api from "./Api.js";
2
2
  import ApiClient from "./ApiClient.js";
3
3
  import * as error from "./error.js";
4
+ import { createDefaultTokenStorageStrategy } from "./utils/createDefaultTokenStorageStrategy.js";
5
+ import { TokenStorageStrategy } from "./utils/TokenStorage.js";
4
6
 
5
- export default { ApiClient, Api, error };
7
+ export default { ApiClient, Api, error, tokenStorageStrategy: { createDefaultTokenStorageStrategy, TokenStorageStrategy } };
@@ -0,0 +1,60 @@
1
+ // src/utils/FileStorageStrategy.node.js
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+
6
+ import { TokenStorageStrategy } from "./TokenStorage.js";
7
+
8
+ export class FileStorageStrategy extends TokenStorageStrategy {
9
+ constructor(filename = "tokens.json", dir = path.join(os.homedir(), ".config", "cocolight")) {
10
+ super();
11
+ this.dir = dir;
12
+ this.filePath = path.join(dir, filename);
13
+ this._ensureDirectoryExists();
14
+ }
15
+
16
+ _ensureDirectoryExists() {
17
+ if (!fs.existsSync(this.dir)) {
18
+ fs.mkdirSync(this.dir, { recursive: true });
19
+ }
20
+ }
21
+
22
+ _readFile() {
23
+ if (!fs.existsSync(this.filePath)) return {};
24
+ try {
25
+ return JSON.parse(fs.readFileSync(this.filePath, "utf8"));
26
+
27
+ } catch (e) {
28
+ console.error("Error reading token file:", e);
29
+ return {};
30
+ }
31
+ }
32
+
33
+ _writeFile(data) {
34
+ fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2), "utf8");
35
+ }
36
+
37
+ getAccessToken() {
38
+ return this._readFile().accessToken || null;
39
+ }
40
+
41
+ setAccessToken(token) {
42
+ const data = this._readFile();
43
+ data.accessToken = token;
44
+ this._writeFile(data);
45
+ }
46
+
47
+ getRefreshToken() {
48
+ return this._readFile().refreshToken || null;
49
+ }
50
+
51
+ setRefreshToken(token) {
52
+ const data = this._readFile();
53
+ data.refreshToken = token;
54
+ this._writeFile(data);
55
+ }
56
+
57
+ clear() {
58
+ this._writeFile({});
59
+ }
60
+ }
@@ -0,0 +1,93 @@
1
+ // src/utils/TokenStorage.js
2
+
3
+ export class TokenStorageStrategy {
4
+ getAccessToken() {
5
+ throw new Error("getAccessToken() doit être implémenté");
6
+ }
7
+
8
+ // eslint-disable-next-line no-unused-vars
9
+ setAccessToken(token) {
10
+ throw new Error("setAccessToken() doit être implémenté");
11
+ }
12
+
13
+ getRefreshToken() {
14
+ throw new Error("getRefreshToken() doit être implémenté");
15
+ }
16
+
17
+ // eslint-disable-next-line no-unused-vars
18
+ setRefreshToken(token) {
19
+ throw new Error("setRefreshToken() doit être implémenté");
20
+ }
21
+
22
+ clear() {
23
+ throw new Error("clear() doit être implémenté");
24
+ }
25
+ }
26
+
27
+ export class MemoryStorageStrategy extends TokenStorageStrategy {
28
+ constructor() {
29
+ super();
30
+ this._accessToken = null;
31
+ this._refreshToken = null;
32
+ }
33
+
34
+ getAccessToken() {
35
+ return this._accessToken;
36
+ }
37
+
38
+ setAccessToken(token) {
39
+ this._accessToken = token;
40
+ }
41
+
42
+ getRefreshToken() {
43
+ return this._refreshToken;
44
+ }
45
+
46
+ setRefreshToken(token) {
47
+ this._refreshToken = token;
48
+ }
49
+
50
+ clear() {
51
+ this._accessToken = null;
52
+ this._refreshToken = null;
53
+ }
54
+ }
55
+
56
+ export class LocalStorageStrategy extends TokenStorageStrategy {
57
+ constructor(prefix = "cocolight") {
58
+ super();
59
+ this.prefix = prefix;
60
+ }
61
+
62
+ getAccessToken() {
63
+ return typeof localStorage !== "undefined"
64
+ ? localStorage.getItem(`${this.prefix}_accessToken`)
65
+ : null;
66
+ }
67
+
68
+ setAccessToken(token) {
69
+ if (typeof localStorage !== "undefined") {
70
+ localStorage.setItem(`${this.prefix}_accessToken`, token);
71
+ }
72
+ }
73
+
74
+ getRefreshToken() {
75
+ return typeof localStorage !== "undefined"
76
+ ? localStorage.getItem(`${this.prefix}_refreshToken`)
77
+ : null;
78
+ }
79
+
80
+ setRefreshToken(token) {
81
+ if (typeof localStorage !== "undefined") {
82
+ localStorage.setItem(`${this.prefix}_refreshToken`, token);
83
+ }
84
+ }
85
+
86
+ clear() {
87
+ if (typeof localStorage !== "undefined") {
88
+ localStorage.removeItem(`${this.prefix}_accessToken`);
89
+ localStorage.removeItem(`${this.prefix}_refreshToken`);
90
+ }
91
+ }
92
+ }
93
+
@@ -0,0 +1,45 @@
1
+ import { LocalStorageStrategy, MemoryStorageStrategy } from "./TokenStorage.js";
2
+
3
+ // src/utils/createDefaultTokenStorageStrategy.js
4
+
5
+ /**
6
+ * Crée une stratégie de stockage de jetons par défaut en fonction de l'option fournie.
7
+ *
8
+ * @param {string} [tokenStorageStrategy="auto"] - La stratégie de stockage de jetons souhaitée.
9
+ * Les valeurs possibles sont :
10
+ * - "memory" : Utilise une stratégie de stockage en mémoire.
11
+ * - "localStorage" : Utilise le localStorage du navigateur (si disponible).
12
+ * - "file" : Utilise une stratégie de stockage basée sur des fichiers (non disponible dans les environnements navigateur).
13
+ * - "auto" : Sélectionne automatiquement la meilleure stratégie de stockage disponible.
14
+ *
15
+ * @returns {Promise<Object>} Une promesse qui se résout avec une instance de la stratégie de stockage sélectionnée.
16
+ *
17
+ * @throws {Error} Lève une erreur si la stratégie sélectionnée n'est pas disponible dans l'environnement actuel.
18
+ */
19
+ export async function createDefaultTokenStorageStrategy(tokenStorageStrategy = "auto") {
20
+ if (tokenStorageStrategy === "memory") {
21
+ return new MemoryStorageStrategy();
22
+ }
23
+ if (tokenStorageStrategy === "localStorage") {
24
+ if (typeof window !== "undefined" && window.localStorage) {
25
+ return new LocalStorageStrategy();
26
+ } else {
27
+ throw new Error("localStorage is not available in this environment.");
28
+ }
29
+ }
30
+ if (tokenStorageStrategy === "file") {
31
+ if (typeof window !== "undefined" && window.localStorage) {
32
+ throw new Error("file storage is not available in this environment.");
33
+ }
34
+ const { FileStorageStrategy } = await import("./FileStorageStrategy.node.js");
35
+ return new FileStorageStrategy();
36
+ }
37
+ if (tokenStorageStrategy === "auto") {
38
+ if (typeof window !== "undefined" && window.localStorage) {
39
+ return new LocalStorageStrategy();
40
+ } else {
41
+ const { FileStorageStrategy } = await import("./FileStorageStrategy.node.js");
42
+ return new FileStorageStrategy();
43
+ }
44
+ }
45
+ }