@communecter/cocolight-api-client 1.0.9 → 1.0.11

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.9",
3
+ "version": "1.0.11",
4
4
  "description": "Client Axios simplifié pour l'API cocolight",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,7 +32,9 @@
32
32
  "generate:testdata": "node ./scripts/generate-test-data.js",
33
33
  "generate:reponses": "node ./scripts/generate-constant-response-200.js",
34
34
  "generate:methodeapi": "node ./scripts/generate-methode-api.js",
35
- "generate:ajv-standalone": "node ./scripts/generate-validate-function-ajv.js"
35
+ "generate:ajv-standalone": "node ./scripts/generate-validate-function-ajv.js",
36
+ "generate:entities": "node scripts/generate-entities.js",
37
+ "generate:all-properties-schema": "node scripts/generate-all-properties-schema.js"
36
38
  },
37
39
  "keywords": [
38
40
  "communecter",
package/src/Api.js CHANGED
@@ -1,10 +1,11 @@
1
1
  // Api.js
2
2
  import EndpointApi from "./api/EndpointApi.js";
3
+ import { News } from "./api/News.js";
3
4
  import { Organization } from "./api/Organization.js";
4
5
  import { Project } from "./api/Project.js";
5
6
  import { User } from "./api/User.js";
6
7
  import { UserApi } from "./api/UserApi.js";
7
- import { ApiAuthenticationError, ApiClientError } from "./error.js";
8
+ import { ApiAuthenticationError, ApiClientError, ApiError } from "./error.js";
8
9
 
9
10
  export default class Api {
10
11
  /**
@@ -50,8 +51,27 @@ export default class Api {
50
51
  */
51
52
  static async userApiLogin(userApi, email, password) {
52
53
  try {
53
- const loggedUser = await userApi.login(email, password);
54
- 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
+
55
75
  } catch (error) {
56
76
  if(error instanceof ApiClientError) {
57
77
  if(error?.details?.error) {
@@ -76,12 +96,14 @@ export default class Api {
76
96
  /**
77
97
  * Retourne l'utilisateur connecté.
78
98
  *
79
- * @returns {User} L'utilisateur connecté.
99
+ * @returns {Promise<User>} L'utilisateur connecté.
100
+ * @throws {ApiAuthenticationError} Si l'utilisateur n'est pas authentifié.
80
101
  */
81
- me() {
102
+ async me() {
82
103
  if (!this._loggedUser) {
83
104
  throw new ApiAuthenticationError("Accès refusé : utilisateur non authentifié.");
84
105
  }
106
+ await this._loggedUser.get();
85
107
  return this._loggedUser;
86
108
  }
87
109
 
@@ -89,11 +111,18 @@ export default class Api {
89
111
  * Crée une instance User pour un utilisateur donné (autre que le connecté).
90
112
  *
91
113
  * @param {Object} userData - Les données de l'utilisateur public.
92
- * @returns {User}
114
+ * @returns {Promise<User>} Une promesse qui résout l'instance User.
115
+ * @throws {Error} Si une erreur se produit lors de la création de l'utilisateur.
93
116
  */
94
- user(userData) {
117
+ async user(userData) {
95
118
  try {
96
- return new User(this._client, userData, {}, { EndpointApi });
119
+ if (!userData.id && !userData.slug) {
120
+ return new User(this._client, userData, { EndpointApi, Organization, Project, News });
121
+ } else {
122
+ const user = new User(this._client, userData, { EndpointApi, Organization, Project, News });
123
+ await user.get();
124
+ return user;
125
+ }
97
126
  } catch (error) {
98
127
  console.error("[Api.user] Erreur lors de la création d'un objet utilisateur public :", error.message);
99
128
  throw error;
@@ -105,18 +134,17 @@ export default class Api {
105
134
  * Creates an Organization object and optionally retrieves its profile.
106
135
  *
107
136
  * @param {Object} organizationData - The data required to initialize the Organization object.
108
- * @param {Object} [options={ getProfile: true }] - Additional options for the organization creation.
109
- * @param {boolean} [options.getProfile=true] - Whether to fetch the organization's profile after creation.
110
137
  * @returns {Promise<Organization>} A promise that resolves to the created Organization object.
111
138
  * @throws {Error} Throws an error if the organization creation or profile retrieval fails.
112
139
  */
113
- async organization(organizationData, options = { getProfile: true }) {
140
+ async organization(organizationData) {
114
141
  try {
115
- const oraganization = new Organization(this._client, organizationData, { EndpointApi });
116
- if (options.getProfile) {
117
- await oraganization.getProfil();
142
+ const organization = new Organization(this._client, organizationData, { EndpointApi, User, Project, News });
143
+ if (!organizationData.id && !organizationData.slug) {
144
+ throw new Error("Vous devez fournir un id ou un slug pour créer une instance Organization.");
118
145
  }
119
- return oraganization;
146
+ await organization.get();
147
+ return organization;
120
148
  } catch (error) {
121
149
  console.error("[Api.organization] Erreur lors de la création d'un objet organisation :", error.message);
122
150
  throw error;
@@ -127,17 +155,16 @@ export default class Api {
127
155
  * Creates a new Project instance and optionally retrieves its profile.
128
156
  *
129
157
  * @param {Object} projectData - The data used to initialize the Project instance.
130
- * @param {Object} [options={ getProfile: true }] - Additional options for project creation.
131
- * @param {boolean} [options.getProfile=true] - Whether to retrieve the project's profile after creation.
132
158
  * @returns {Promise<Project>} A promise that resolves to the created Project instance.
133
159
  * @throws {Error} If an error occurs during project creation or profile retrieval.
134
160
  */
135
- async project(projectData, options = { getProfile: true }) {
161
+ async project(projectData) {
136
162
  try {
137
- const project = new Project(this._client, projectData, { EndpointApi });
138
- if (options.getProfile) {
139
- await project.getProfil();
163
+ const project = new Project(this._client, projectData, { User, News, EndpointApi });
164
+ if (!projectData.id && !projectData.slug) {
165
+ throw new Error("Vous devez fournir un id ou un slug pour créer une instance Project.");
140
166
  }
167
+ await project.get();
141
168
  return project;
142
169
  } catch (error) {
143
170
  console.error("[Api.project] Erreur lors de la création d'un objet projet :", error.message);
@@ -150,13 +177,23 @@ export default class Api {
150
177
  *
151
178
  * @returns {ApiClient}
152
179
  */
153
-
154
180
  get client() {
155
181
  return this._client;
156
182
  }
157
183
 
184
+ /**
185
+ * Retourne l'instance d'EndpointApi.
186
+ *
187
+ * @returns {EndpointApi}
188
+ */
158
189
  get endpointApi() {
159
190
  return new EndpointApi(this._client);
160
191
  }
192
+
193
+ logout () {
194
+ this.loggedUser = null;
195
+ this._client.resetSession();
196
+ this._client._logger.info("UserApi: User logged out");
197
+ }
161
198
 
162
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
 
@@ -51,8 +54,9 @@ export default class ApiClient extends EventEmitter {
51
54
  throw new ApiClientError("Le paramètre \"baseURL\" est obligatoire.", 500);
52
55
  }
53
56
 
57
+ this.__entityTag = "ApiClient";
58
+
54
59
  this._baseURL = baseURL;
55
- this._refreshToken = refreshToken;
56
60
  this._refreshUrl = refreshUrl;
57
61
  this._endpoints = endpoints;
58
62
  this._debug = debug;
@@ -86,7 +90,6 @@ export default class ApiClient extends EventEmitter {
86
90
  });
87
91
 
88
92
  // Pino logger
89
- // (Ici en mode pretty-print sur la console, tu peux configurer comme tu veux)
90
93
  this._logger = pino({
91
94
  transport: {
92
95
  target: "pino-pretty",
@@ -121,30 +124,73 @@ export default class ApiClient extends EventEmitter {
121
124
  this._breakerOpen = false;
122
125
  this._lastBreakerOpenTime = null;
123
126
 
124
- // 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
+
125
135
  if (accessToken) {
126
136
  this.setToken(accessToken);
127
137
  }
128
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
+
129
152
  // Intercepteur 401 -> refresh
130
153
  this._client.interceptors.response.use(
131
- (response) => response,
132
- async (error) => {
133
- if (error.response && error.response.status === 401) {
134
- if (this._refreshToken) {
135
- try {
136
- const refreshed = await this._refreshAccessToken();
137
- if (refreshed) {
138
- return this._client.request(error.config);
139
- }
140
- } catch (err) {
141
- 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);
142
182
  }
183
+ } catch (err) {
184
+ this.resetSession();
185
+ throw new ApiClientError("Erreur lors du rafraîchissement du token.", 401, err);
143
186
  }
144
187
  }
188
+
145
189
  throw error;
146
190
  }
147
191
  );
192
+
193
+
148
194
  }
149
195
 
150
196
  /**
@@ -154,6 +200,7 @@ export default class ApiClient extends EventEmitter {
154
200
  */
155
201
  setToken(token) {
156
202
  this._accessToken = token;
203
+ this._tokenStorage.setAccessToken(token);
157
204
  this._client.defaults.headers.common["Authorization"] = "Bearer " + token;
158
205
  // Extrait l'id depuis le token et le stocke si disponible
159
206
  const userId = this._getIdFromToken(token);
@@ -176,17 +223,20 @@ export default class ApiClient extends EventEmitter {
176
223
  /**
177
224
  * Sets the refresh token for the API client.
178
225
  *
179
- * @param {string} rt - The refresh token to be set.
226
+ * @param {string} refreshToken - The refresh token to be set.
180
227
  */
181
- setRefreshToken(rt) {
182
- this._refreshToken = rt;
183
- // Vous pouvez faire de même ici si besoin
184
- const userId = this._getIdFromToken(rt);
185
- if (userId) {
186
- this._setUserId(userId);
187
- 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
+ }
188
238
  }
189
- this._logger.debug(`[ApiClient] setRefreshToken: ${rt}`);
239
+ this._logger.debug(`[ApiClient] setRefreshToken: ${token}`);
190
240
  }
191
241
 
192
242
  /**
@@ -234,14 +284,19 @@ export default class ApiClient extends EventEmitter {
234
284
  */
235
285
  async _refreshAccessToken() {
236
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
+
237
294
  try {
238
- const response = await this._client.post(
239
- this._refreshUrl,
240
- { refreshToken: this._refreshToken },
241
- { headers: { "Content-Type": "application/json" } }
242
- );
243
- if (response.data && response.data.accessToken) {
244
- 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);
245
300
  if (response.data.refreshToken) {
246
301
  this.setRefreshToken(response.data.refreshToken);
247
302
  }
@@ -774,6 +829,7 @@ export default class ApiClient extends EventEmitter {
774
829
  this.setToken(null);
775
830
  this.setRefreshToken(null);
776
831
  this._setUserId(null);
832
+ this._tokenStorage.clear();
777
833
 
778
834
  // Suppression des en-têtes
779
835
  delete this._client.defaults.headers.common["Authorization"];
@@ -1379,5 +1435,13 @@ export default class ApiClient extends EventEmitter {
1379
1435
  };
1380
1436
 
1381
1437
 
1438
+ getRequestSchema(constant) {
1439
+ const endpoint = this._endpoints.find(e => e.constant === constant);
1440
+ return endpoint?.request || null;
1441
+ }
1442
+
1443
+ getPathSchema(constant) {
1444
+ return this._endpoints.find(e => e.constant === constant)?.pathParams || null;
1445
+ }
1382
1446
 
1383
1447
  }