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