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.
- package/README.md +25 -0
- package/REST.ts +144 -16
- 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
|
-
|
|
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')
|
|
933
|
+
if (typeof window.crypto?.subtle === 'undefined') return Promise.resolve("");
|
|
806
934
|
|
|
807
935
|
return window.crypto.subtle
|
|
808
936
|
.importKey(
|