@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/README.md +281 -1
- package/dist/405.cocolight-api-client.browser.js +1 -0
- package/dist/405.cocolight-api-client.cjs +1 -0
- package/dist/790.cocolight-api-client.mjs.js +1 -0
- package/dist/cocolight-api-client.browser.js +3 -3
- package/dist/cocolight-api-client.cjs +1 -1
- package/dist/cocolight-api-client.mjs.js +1 -1
- package/package.json +4 -2
- package/src/Api.js +59 -22
- package/src/ApiClient.js +95 -31
- package/src/api/News.js +129 -88
- package/src/api/Organization.js +248 -166
- package/src/api/Project.js +263 -172
- package/src/api/User.js +355 -49
- package/src/api/UserApi.js +25 -2
- package/src/endpoints.module.js +1 -1
- package/src/index.js +3 -1
- package/src/mixin/DraftStateMixin.js +176 -0
- package/src/mixin/EntityMixin.js +96 -35
- package/src/mixin/MutualEntityMixin.js +48 -0
- package/src/mixin/NewsMixin.js +35 -1
- package/src/mixin/UtilMixin.js +49 -1
- package/src/utils/FileStorageStrategy.node.js +60 -0
- package/src/utils/TokenStorage.js +93 -0
- package/src/utils/createDefaultTokenStorageStrategy.js +45 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@communecter/cocolight-api-client",
|
|
3
|
-
"version": "1.0.
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
|
140
|
+
async organization(organizationData) {
|
|
114
141
|
try {
|
|
115
|
-
const
|
|
116
|
-
if (
|
|
117
|
-
|
|
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
|
-
|
|
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
|
|
161
|
+
async project(projectData) {
|
|
136
162
|
try {
|
|
137
|
-
const project = new Project(this._client, projectData, { EndpointApi });
|
|
138
|
-
if (
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
async
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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}
|
|
226
|
+
* @param {string} refreshToken - The refresh token to be set.
|
|
180
227
|
*/
|
|
181
|
-
setRefreshToken(
|
|
182
|
-
this._refreshToken =
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
this.
|
|
187
|
-
|
|
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: ${
|
|
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
|
|
239
|
-
this.
|
|
240
|
-
|
|
241
|
-
|
|
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
|
}
|