flamerest 1.0.125 → 1.0.126

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.
Files changed (3) hide show
  1. package/README.md +25 -0
  2. package/REST.ts +144 -16
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -18,6 +18,31 @@ import FLAMEREST from "flamerest";
18
18
  window.REST = new FLAMEREST('localhost');
19
19
  ```
20
20
 
21
+ ## Callbacks
22
+
23
+ ### Unauthorized Callback
24
+ Обработка 401 ошибок (неавторизован):
25
+ ```javascript
26
+ const rest = new FLAMEREST('https://api.example.com', undefined, () => {
27
+ // Пользователь не авторизован - перенаправляем на страницу входа
28
+ window.location.href = '/login';
29
+ });
30
+ ```
31
+
32
+ ### Maintenance Callback
33
+ Обработка 503 ошибок (сервер на обслуживании):
34
+ ```javascript
35
+ const rest = new FLAMEREST('https://api.example.com', undefined, undefined, 'v1', (errorMessage) => {
36
+ // Сервер на обслуживании
37
+ if (errorMessage) {
38
+ alert(`Сервер временно недоступен: ${errorMessage}`);
39
+ } else {
40
+ alert('Сервер временно недоступен. Попробуйте позже.');
41
+ }
42
+ // Можно показать модальное окно с сообщением об обслуживании
43
+ });
44
+ ```
45
+
21
46
  # Create and Update
22
47
 
23
48
  Just field list, but any field can be HTML Input [type=file], Clipboard object, DataTransfer [Drag&Drop/Clipboard] or FileList (automatically upload any of that)
package/REST.ts CHANGED
@@ -39,6 +39,15 @@ export type CustomAnyRequest = BaseResponse<any>;
39
39
  */
40
40
  export type SavedObject<T> = BaseResponse<T>;
41
41
 
42
+ /**
43
+ * Элемент баланса пользователя
44
+ */
45
+ export interface BalanceItem {
46
+ name: string;
47
+ balance_decimal: string;
48
+ value: number;
49
+ }
50
+
42
51
  /**
43
52
  * Результат авторизации
44
53
  */
@@ -51,12 +60,16 @@ export type Authorized = {
51
60
  lang: string,
52
61
  name: string,
53
62
  role: string,
54
- user_hash?: string
63
+ user_hash?: string,
64
+ balance: 0,
65
+ balance_all: { [key: string]: BalanceItem },
55
66
  };
56
67
  errors: { [key: string]: any };
57
68
  token: string;
58
69
  }
59
70
 
71
+
72
+
60
73
  export class FLAMEREST {
61
74
 
62
75
  /**
@@ -74,6 +87,12 @@ export class FLAMEREST {
74
87
  */
75
88
  public unauthorized_callback: (() => void) | undefined;
76
89
 
90
+ /**
91
+ * Будет вызван, если сервер вернёт статус 503 (Service Unavailable / Maintenance)
92
+ * Может содержать сообщение об ошибке из JSON ответа
93
+ */
94
+ public maintenance_callback: ((errorMessage?: string) => void) | undefined;
95
+
77
96
  /**
78
97
  * Токен приложения конкретного клиента для отправки пуш уведомлений именно ему
79
98
  */
@@ -97,7 +116,15 @@ export class FLAMEREST {
97
116
  public token: string | null = null;
98
117
 
99
118
 
100
- constructor(server_address?: string, localhost_endpoint?: string, unauthorized_callback?: () => void, version?: string) {
119
+ /**
120
+ * Конструктор класса FLAMEREST
121
+ * @param {string} server_address Адрес сервера (опционально)
122
+ * @param {string} localhost_endpoint Альтернативный адрес для localhost (опционально)
123
+ * @param {Function} unauthorized_callback Колбек для обработки 401 ошибок (опционально)
124
+ * @param {string} version Версия API (по умолчанию 'v1')
125
+ * @param {Function} maintenance_callback Колбек для обработки 503 ошибок (опционально)
126
+ */
127
+ constructor(server_address?: string, localhost_endpoint?: string, unauthorized_callback?: () => void, version?: string, maintenance_callback?: (errorMessage?: string) => void) {
101
128
  if (typeof window === 'undefined') {
102
129
  // SSR: используем предоставленный адрес или дефолтный
103
130
  this.SERVER = server_address ?? "http://localhost/";
@@ -118,6 +145,7 @@ export class FLAMEREST {
118
145
  this.SERVER = this.SERVER.endsWith('/') ? this.SERVER.substring(0, this.SERVER.length - 1) : this.SERVER;
119
146
 
120
147
  this.unauthorized_callback = unauthorized_callback;
148
+ this.maintenance_callback = maintenance_callback;
121
149
  this.version = version ?? 'v1';
122
150
  }
123
151
 
@@ -141,7 +169,15 @@ export class FLAMEREST {
141
169
  * @param {boolean} isNeedToken нужен ли токен для запроса
142
170
  * @param {Object} customHeaders объект с доп заголовками, которые надо включить в запрос
143
171
  */
144
- request(url: string, params: object | string | FormData, type: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET', responseType: 'json' | 'blob' = 'json', isNeedToken = true, customHeaders: Record<string, string> = {}): Promise<BaseResponse<any> & Partial<Paginated>> {
172
+ async request(url: string, params: object | string | FormData, type: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET', responseType: 'json' | 'blob' = 'json', isNeedToken = true, customHeaders: Record<string, string> = {}): Promise<BaseResponse<any> & Partial<Paginated>> {
173
+
174
+ // Подставляем сервер автоматом в запрос начинающийся с /
175
+ if (url[0] === '/' && url[1] !== '/') {
176
+ url = this.SERVER + url;
177
+ }
178
+
179
+ // Создаём подпись запроса до его модификации
180
+ const SIGN = await this._generateRequestSignature(url, params);
145
181
 
146
182
  // Нормализуем параметры, если они есть
147
183
  if (typeof params === "object" && params !== null) {
@@ -155,10 +191,6 @@ export class FLAMEREST {
155
191
  responseType = 'json';
156
192
  }
157
193
 
158
- // Подставляем сервер автоматом в запрос начинающийся с /
159
- if (url[0] === '/' && url[1] !== '/') {
160
- url = this.SERVER + url;
161
- }
162
194
 
163
195
  const that = this;
164
196
 
@@ -204,14 +236,7 @@ export class FLAMEREST {
204
236
  requestBody.body = params;
205
237
  }
206
238
 
207
- // Создаём подпись каждого запроса
208
- let SIGN = "empty";
209
- try {
210
- // Используем user_hash для подписи, если он есть
211
- SIGN = await this.hmac_sha256(requestBody.body, user_hash ?? "");
212
- } catch (ex) {
213
- // console.error('Can`t create a request sign');
214
- }
239
+ // Добавляем подпись в заголовки
215
240
  (requestBody.headers as any)['sign'] = SIGN;
216
241
 
217
242
  // Делаем запрос
@@ -244,6 +269,16 @@ export class FLAMEREST {
244
269
  this.unauthorized_callback();
245
270
  }
246
271
  break;
272
+ case 503:
273
+ // Обработка режима обслуживания (maintenance)
274
+ let maintenanceMessage: string | undefined = undefined;
275
+ if (ResolveBody.errors && typeof ResolveBody.errors === 'object' && 'error' in ResolveBody.errors) {
276
+ maintenanceMessage = (ResolveBody.errors as any).error;
277
+ }
278
+ if (typeof this.maintenance_callback === 'function') {
279
+ this.maintenance_callback(maintenanceMessage);
280
+ }
281
+ break;
247
282
  }
248
283
  } catch (exjson) {
249
284
  ResolveBody.errors = await response.text();
@@ -776,6 +811,98 @@ export class FLAMEREST {
776
811
  return result;
777
812
  }
778
813
 
814
+ /**
815
+ * Вычисляет на лету детерминированную строку, которая будет использоваться как часть ключа для подписи.
816
+ * Ключ генерируется на основе последовательности числовых преобразований, что делает его неочевидным
817
+ * при статическом анализе кода. Этот метод всегда возвращает один и тот же результат.
818
+ * @returns {string} Детерминированно сгенерированная строка.
819
+ */
820
+ getDeterministicKeyPart(): string {
821
+ // Начальная последовательность чисел. Она может быть любой, главное - постоянной.
822
+ const sequence = [5, 12, 23, 8, 17, 3, 29, 14, 31, 2, 11, 28, 7, 20, 1, 16, 22, 5, 13, 19, 4, 25, 10, 30, 9, 21, 6, 18, 27, 15, 24, 26];
823
+ // "Магическое число" для усложнения преобразований.
824
+ const magicNumber = 177;
825
+ let result = '';
826
+
827
+ for (let i = 0; i < sequence.length; i++) {
828
+ const num = sequence[i];
829
+ let charCode;
830
+
831
+ // В зависимости от позиции применяем разные преобразования,
832
+ // чтобы получить символы в разных регистрах и цифры.
833
+ switch (i % 3) {
834
+ case 0:
835
+ // Генерируем заглавную букву (A-Z)
836
+ charCode = (num * magicNumber + i) % 26;
837
+ result += String.fromCharCode(65 + charCode);
838
+ break;
839
+ case 1:
840
+ // Генерируем строчную букву (a-z)
841
+ charCode = (num + magicNumber * (i + 1)) % 26;
842
+ result += String.fromCharCode(97 + charCode);
843
+ break;
844
+ case 2:
845
+ // Генерируем цифру (0-9)
846
+ charCode = (num * (i + 1) + magicNumber) % 10;
847
+ result += String.fromCharCode(48 + charCode);
848
+ break;
849
+ }
850
+ }
851
+ return result;
852
+ }
853
+
854
+ /**
855
+ * Создаёт подпись для запроса на основе его параметров, используя лучшие практики.
856
+ * Собирает все GET и POST параметры, сортирует их и формирует каноническую строку для хеширования.
857
+ * @param url URL запроса, включая GET-параметры.
858
+ * @param params POST-параметры запроса (в виде объекта).
859
+ * @returns Promise<string> Строка подписи.
860
+ */
861
+ private async _generateRequestSignature(url: string, params: object | string | FormData): Promise<string> {
862
+ const secretKey = (this.token ?? '') + this.getDeterministicKeyPart();
863
+
864
+ // Если параметры - это FormData или не объект, мы не можем их надежно обработать для подписи.
865
+ // В этом случае подписываем пустую строку, как и раньше.
866
+ if (params instanceof FormData || typeof params !== 'object' || params === null) {
867
+ return this.hmac_sha256('', secretKey);
868
+ }
869
+
870
+ const allParams: { [key: string]: any } = {};
871
+
872
+ // 1. Извлекаем GET-параметры из URL
873
+ if (url.includes('?')) {
874
+ const queryString = url.split('?')[1];
875
+ const urlParams = new URLSearchParams(queryString);
876
+ urlParams.forEach((value, key) => {
877
+ allParams[key] = value;
878
+ });
879
+ }
880
+
881
+ // 2. Добавляем POST-параметры из тела запроса
882
+ Object.assign(allParams, params);
883
+
884
+ // 3. Создаём каноническую строку
885
+ // Сортируем ключи для консистентности
886
+ const sortedKeys = Object.keys(allParams).sort();
887
+
888
+ // Формируем строку вида "key1=value1&key2=value2..."
889
+ const canonicalString = sortedKeys
890
+ .map(key => {
891
+ const value = allParams[key];
892
+ // Пропускаем null и undefined значения, они не участвуют в подписи
893
+ if (value === null || value === undefined) {
894
+ return '';
895
+ }
896
+ // Если значение - объект (включая массив), заменяем его на слово "object" для консистентности подписи.
897
+ const stringValue = (typeof value === 'object' && value !== null) ? 'object' : String(value);
898
+ return `${encodeURIComponent(key)}=${encodeURIComponent(stringValue)}`;
899
+ })
900
+ .filter(part => part !== '') // Убираем пустые части
901
+ .join('&');
902
+
903
+ return this.hmac_sha256(canonicalString, secretKey);
904
+ }
905
+
779
906
  /**
780
907
  *
781
908
  * @param {string} data
@@ -797,12 +924,13 @@ export class FLAMEREST {
797
924
  if (typeof window === 'undefined') {
798
925
  // Динамически импортируем модуль 'crypto' для Node.js, чтобы избежать ошибок на фронтенде,
799
926
  // где этот модуль недоступен.
927
+ // @ts-ignore
800
928
  const { createHmac } = await import('crypto');
801
929
  const hash = createHmac('sha256', key).update(data).digest('hex');
802
930
  return Promise.resolve(hash);
803
931
  } else {
804
932
 
805
- if (typeof window.crypto?.subtle === 'undefined') throw new Error("Can`t create hash");
933
+ if (typeof window.crypto?.subtle === 'undefined') return Promise.resolve("");
806
934
 
807
935
  return window.crypto.subtle
808
936
  .importKey(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flamerest",
3
- "version": "1.0.125",
3
+ "version": "1.0.126",
4
4
  "description": "",
5
5
  "main": "./REST.ts",
6
6
  "typings": "./REST.ts",