@apptimate/core-lib 1.0.0 → 1.2.0
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/CHANGELOG.md +7 -7
- package/package.json +16 -16
- package/src/client.ts +10 -0
- package/src/common/interfaces/ICommon.ts +6 -6
- package/src/constants/iconRegistry.ts +189 -0
- package/src/constants/menus.ts +576 -0
- package/src/constants/storageKeys.ts +14 -9
- package/src/index.ts +3 -8
- package/src/server.ts +12 -0
- package/src/utils/apiProxy.ts +155 -0
- package/src/utils/bem.ts +13 -13
- package/src/utils/bootstrapConfig.ts +80 -0
- package/src/utils/cn.ts +9 -9
- package/src/utils/commonService.ts +46 -26
- package/src/utils/cookiesHandler.ts +64 -64
- package/src/utils/httpClient.ts +192 -161
- package/src/utils/localStorageHandler.ts +59 -49
package/src/utils/httpClient.ts
CHANGED
|
@@ -1,161 +1,192 @@
|
|
|
1
|
-
import { IApiResponse } from "../common/interfaces/ICommon";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Handle
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
1
|
+
import { IApiResponse } from "../common/interfaces/ICommon";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Handles 401 Unauthorized responses globally.
|
|
5
|
+
* Clears the server-managed session and app-owned local state, then redirects
|
|
6
|
+
* to the login page.
|
|
7
|
+
*/
|
|
8
|
+
export function handleUnauthorized(): void {
|
|
9
|
+
// Best-effort server-side cookie clear for HttpOnly auth cookies.
|
|
10
|
+
if (typeof window !== 'undefined') {
|
|
11
|
+
fetch('/api/session/logout', {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
credentials: 'same-origin',
|
|
14
|
+
keepalive: true,
|
|
15
|
+
}).catch(() => {
|
|
16
|
+
// Ignore logout cleanup failures and proceed with client-side reset.
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Clear only app-owned localStorage keys instead of wiping the entire origin
|
|
21
|
+
if (typeof window !== 'undefined') {
|
|
22
|
+
const appKeys = ['selected_organization'];
|
|
23
|
+
// Also clear any key from our storageKeys registry
|
|
24
|
+
try {
|
|
25
|
+
const allKeys = Object.keys(localStorage);
|
|
26
|
+
allKeys.forEach(key => {
|
|
27
|
+
if (appKeys.includes(key) || key.startsWith('apptimate_') || key.startsWith('app_')) {
|
|
28
|
+
localStorage.removeItem(key);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
} catch {
|
|
32
|
+
// Silently ignore storage access errors
|
|
33
|
+
}
|
|
34
|
+
window.location.href = '/auth/login';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
export interface RequestOptions {
|
|
40
|
+
url: string;
|
|
41
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
42
|
+
data?: any;
|
|
43
|
+
params?: Record<string, any>;
|
|
44
|
+
headers?: Record<string, string>;
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ClientResponse<T = any> {
|
|
49
|
+
ok: boolean;
|
|
50
|
+
status: number;
|
|
51
|
+
responseData: IApiResponse<T>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function buildBrowserProxyUrl(rawUrl: string): string {
|
|
55
|
+
if (typeof window === 'undefined') {
|
|
56
|
+
return rawUrl;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const parsedUrl = new URL(rawUrl, window.location.origin);
|
|
61
|
+
|
|
62
|
+
if (parsedUrl.origin === window.location.origin) {
|
|
63
|
+
return parsedUrl.toString();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const proxyPath = parsedUrl.pathname.replace(/^\/+/, '');
|
|
67
|
+
return `/api/proxy/${proxyPath}${parsedUrl.search}`;
|
|
68
|
+
} catch {
|
|
69
|
+
return rawUrl;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function sendRequest<T = any>({ url, method, data, params, headers, signal }: RequestOptions): Promise<ClientResponse<T>> {
|
|
74
|
+
const defaultHeaders: Record<string, string> = {
|
|
75
|
+
'Accept': 'application/json',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Serialize params into query string
|
|
79
|
+
let finalUrl = url;
|
|
80
|
+
if (params) {
|
|
81
|
+
const queryParams = new URLSearchParams();
|
|
82
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
83
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
84
|
+
queryParams.append(key, String(value));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
const queryString = queryParams.toString();
|
|
88
|
+
if (queryString) {
|
|
89
|
+
finalUrl += (url.includes('?') ? '&' : '?') + queryString;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Client-side requests are routed through a same-origin proxy so the
|
|
94
|
+
// browser never needs direct access to the bearer token.
|
|
95
|
+
if (typeof window !== 'undefined') {
|
|
96
|
+
finalUrl = buildBrowserProxyUrl(finalUrl);
|
|
97
|
+
|
|
98
|
+
// Automatically inject selected organization ID into every request
|
|
99
|
+
try {
|
|
100
|
+
const orgRaw = localStorage.getItem('selected_organization');
|
|
101
|
+
if (orgRaw) {
|
|
102
|
+
const org = JSON.parse(orgRaw);
|
|
103
|
+
if (org?.id) {
|
|
104
|
+
defaultHeaders['X-Organization-Id'] = String(org.id);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
// silently ignore parse errors
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (data && !(data instanceof FormData)) {
|
|
113
|
+
defaultHeaders['Content-Type'] = 'application/json';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const response = await fetch(finalUrl, {
|
|
118
|
+
method,
|
|
119
|
+
headers: { ...defaultHeaders, ...headers },
|
|
120
|
+
body: data instanceof FormData ? data : (data ? JSON.stringify(data) : undefined),
|
|
121
|
+
signal,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Handle 401 Unauthorized globally — clear auth and redirect to login
|
|
125
|
+
const isAuthEndpoint = finalUrl.includes('/auth/login') || finalUrl.includes('/auth/register') || finalUrl.includes('/auth/me');
|
|
126
|
+
|
|
127
|
+
if (response.status === 401 && typeof window !== 'undefined' && !isAuthEndpoint) {
|
|
128
|
+
handleUnauthorized();
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
status: 401,
|
|
132
|
+
responseData: {
|
|
133
|
+
is_success: false,
|
|
134
|
+
message: 'Unauthenticated.',
|
|
135
|
+
result: null,
|
|
136
|
+
system_code: 'unauthenticated'
|
|
137
|
+
} as IApiResponse<T>,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let responseData: any;
|
|
142
|
+
const contentType = response.headers.get('content-type');
|
|
143
|
+
|
|
144
|
+
if (contentType && contentType.includes('application/json')) {
|
|
145
|
+
responseData = await response.json();
|
|
146
|
+
} else {
|
|
147
|
+
responseData = {
|
|
148
|
+
is_success: response.ok,
|
|
149
|
+
message: response.statusText,
|
|
150
|
+
result: null,
|
|
151
|
+
system_code: ''
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Handle inactive user — force logout and redirect to login
|
|
156
|
+
// Skip for auth endpoints so the login form can display the error
|
|
157
|
+
if (responseData?.system_code === 'user_inactive' && typeof window !== 'undefined' && !isAuthEndpoint) {
|
|
158
|
+
handleUnauthorized();
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
status: 403,
|
|
162
|
+
responseData: {
|
|
163
|
+
is_success: false,
|
|
164
|
+
message: responseData.message || 'Your account has been deactivated.',
|
|
165
|
+
result: null,
|
|
166
|
+
system_code: 'user_inactive'
|
|
167
|
+
} as IApiResponse<T>,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
ok: response.ok,
|
|
173
|
+
status: response.status,
|
|
174
|
+
responseData: responseData as IApiResponse<T>,
|
|
175
|
+
};
|
|
176
|
+
} catch (error: any) {
|
|
177
|
+
if (error.name === 'AbortError') {
|
|
178
|
+
throw error; // Rethrow to be caught by specific timeout handling
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
ok: false,
|
|
183
|
+
status: 0,
|
|
184
|
+
responseData: {
|
|
185
|
+
is_success: false,
|
|
186
|
+
message: error.message || 'Network error',
|
|
187
|
+
result: null,
|
|
188
|
+
system_code: 'network_failure'
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -1,49 +1,59 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { IStorageOptions } from '../constants/storageKeys';
|
|
4
|
-
import { decrypt, encrypt, replacePlaceholders } from './commonService';
|
|
5
|
-
|
|
6
|
-
export function getLocalStorage<T = any>(storageKey: IStorageOptions, options?: { replacements?: Record<string, string | number> }): T | null {
|
|
7
|
-
if (typeof window === 'undefined') return null;
|
|
8
|
-
|
|
9
|
-
const name = replacePlaceholders(storageKey.encrypted ? storageKey.secretName : storageKey.name, options?.replacements || {});
|
|
10
|
-
const value = localStorage.getItem(name);
|
|
11
|
-
|
|
12
|
-
if (!value) return null;
|
|
13
|
-
|
|
14
|
-
if (storageKey.encrypted) {
|
|
15
|
-
return decrypt(value) as T;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
return JSON.parse(value) as T;
|
|
20
|
-
} catch {
|
|
21
|
-
return value as unknown as T;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function setLocalStorage(storageKey: IStorageOptions, value: any, options?: { replacements?: Record<string, string | number> }): void {
|
|
26
|
-
if (typeof window === 'undefined') return;
|
|
27
|
-
|
|
28
|
-
const name = replacePlaceholders(storageKey.encrypted ? storageKey.secretName : storageKey.name, options?.replacements || {});
|
|
29
|
-
|
|
30
|
-
let finalValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
31
|
-
if (storageKey.encrypted) {
|
|
32
|
-
finalValue = encrypt(value);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
localStorage.setItem(name, finalValue);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function clearLocalStorage(storageKey: IStorageOptions, options?: { replacements?: Record<string, string | number> }): void {
|
|
39
|
-
if (typeof window === 'undefined') return;
|
|
40
|
-
|
|
41
|
-
const name = replacePlaceholders(storageKey.encrypted ? storageKey.secretName : storageKey.name, options?.replacements || {});
|
|
42
|
-
localStorage.removeItem(name);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function clearAllLocalStorage(): void {
|
|
46
|
-
if (typeof window !== 'undefined') {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { IStorageOptions } from '../constants/storageKeys';
|
|
4
|
+
import { decrypt, encrypt, replacePlaceholders } from './commonService';
|
|
5
|
+
|
|
6
|
+
export function getLocalStorage<T = any>(storageKey: IStorageOptions, options?: { replacements?: Record<string, string | number> }): T | null {
|
|
7
|
+
if (typeof window === 'undefined') return null;
|
|
8
|
+
|
|
9
|
+
const name = replacePlaceholders(storageKey.encrypted ? storageKey.secretName : storageKey.name, options?.replacements || {});
|
|
10
|
+
const value = localStorage.getItem(name);
|
|
11
|
+
|
|
12
|
+
if (!value) return null;
|
|
13
|
+
|
|
14
|
+
if (storageKey.encrypted) {
|
|
15
|
+
return decrypt(value) as T;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(value) as T;
|
|
20
|
+
} catch {
|
|
21
|
+
return value as unknown as T;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function setLocalStorage(storageKey: IStorageOptions, value: any, options?: { replacements?: Record<string, string | number> }): void {
|
|
26
|
+
if (typeof window === 'undefined') return;
|
|
27
|
+
|
|
28
|
+
const name = replacePlaceholders(storageKey.encrypted ? storageKey.secretName : storageKey.name, options?.replacements || {});
|
|
29
|
+
|
|
30
|
+
let finalValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
31
|
+
if (storageKey.encrypted) {
|
|
32
|
+
finalValue = encrypt(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
localStorage.setItem(name, finalValue);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function clearLocalStorage(storageKey: IStorageOptions, options?: { replacements?: Record<string, string | number> }): void {
|
|
39
|
+
if (typeof window === 'undefined') return;
|
|
40
|
+
|
|
41
|
+
const name = replacePlaceholders(storageKey.encrypted ? storageKey.secretName : storageKey.name, options?.replacements || {});
|
|
42
|
+
localStorage.removeItem(name);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function clearAllLocalStorage(): void {
|
|
46
|
+
if (typeof window !== 'undefined') {
|
|
47
|
+
const appKeys = ['selected_organization'];
|
|
48
|
+
try {
|
|
49
|
+
const allKeys = Object.keys(localStorage);
|
|
50
|
+
allKeys.forEach(key => {
|
|
51
|
+
if (appKeys.includes(key) || key.startsWith('apptimate_') || key.startsWith('app_')) {
|
|
52
|
+
localStorage.removeItem(key);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
} catch {
|
|
56
|
+
// Silently ignore storage access errors
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|