@apptimate/core-lib 1.0.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 -0
- package/package.json +17 -0
- package/src/common/interfaces/ICommon.ts +6 -0
- package/src/constants/storageKeys.ts +9 -0
- package/src/index.ts +8 -0
- package/src/utils/bem.ts +13 -0
- package/src/utils/cn.ts +9 -0
- package/src/utils/commonService.ts +26 -0
- package/src/utils/cookiesHandler.ts +64 -0
- package/src/utils/httpClient.ts +161 -0
- package/src/utils/localStorageHandler.ts +49 -0
package/CHANGELOG.md
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@apptimate/core-lib",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "src/index.ts",
|
|
5
|
+
"types": "src/index.ts",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"js-cookie": "^3.0.5",
|
|
11
|
+
"clsx": "^2.1.1",
|
|
12
|
+
"tailwind-merge": "^3.5.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/js-cookie": "^3.0.6"
|
|
16
|
+
}
|
|
17
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './utils/commonService';
|
|
2
|
+
export * from './utils/cookiesHandler';
|
|
3
|
+
export * from './utils/httpClient';
|
|
4
|
+
export * from './utils/localStorageHandler';
|
|
5
|
+
export * from './utils/bem';
|
|
6
|
+
export * from './utils/cn';
|
|
7
|
+
export * from './common/interfaces/ICommon';
|
|
8
|
+
export * from './constants/storageKeys';
|
package/src/utils/bem.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function block(prefix: string, name: string): string {
|
|
2
|
+
return `${prefix}-${name}`;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function element(prefix: string, blockName: string, elementName: string): string {
|
|
6
|
+
return `${prefix}-${blockName}__${elementName}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function modifier(prefix: string, name: string, modifier: string | undefined | null | boolean): string {
|
|
10
|
+
if (!modifier) return "";
|
|
11
|
+
if (typeof modifier === "boolean") return modifier ? `${prefix}-${name}--active` : "";
|
|
12
|
+
return `${prefix}-${name}--${modifier}`;
|
|
13
|
+
}
|
package/src/utils/cn.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function replacePlaceholders(template: string, replacements: Record<string, string | number>): string {
|
|
2
|
+
return template.replace(/\${(\w+)}/g, (_, key) => {
|
|
3
|
+
return (replacements[key] ?? `\${${key}}`).toString();
|
|
4
|
+
});
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// For now, these are simple passthrough since a specific encryption library/secret wasn't provided.
|
|
8
|
+
// The skill requires them, so we implement them so code works, even if weak until secret is present.
|
|
9
|
+
export function encrypt(value: any): string {
|
|
10
|
+
const stringValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
11
|
+
// In production, use process.env.CRYPT_SECRET_KEY and an encryption lib (e.g. crypto-js or node-crypto)
|
|
12
|
+
return btoa(unescape(encodeURIComponent(stringValue)));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function decrypt(encryptedValue: string): any {
|
|
16
|
+
try {
|
|
17
|
+
const decrypted = decodeURIComponent(escape(atob(encryptedValue)));
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(decrypted);
|
|
20
|
+
} catch {
|
|
21
|
+
return decrypted;
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
import { cookies } from 'next/headers';
|
|
4
|
+
import { IStorageOptions } from '../constants/storageKeys';
|
|
5
|
+
import { decrypt, encrypt, replacePlaceholders } from './commonService';
|
|
6
|
+
|
|
7
|
+
export async function getCookies<T = any>(storageKey: IStorageOptions, options?: { replacements?: Record<string, string | number> }): Promise<T | null> {
|
|
8
|
+
const cookieStore = await cookies();
|
|
9
|
+
const name = replacePlaceholders(storageKey.encrypted ? storageKey.secretName : storageKey.name, options?.replacements || {});
|
|
10
|
+
|
|
11
|
+
const value = cookieStore.get(name)?.value;
|
|
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 async function setCookies(storageKey: IStorageOptions, value: any, options?: {
|
|
26
|
+
replacements?: Record<string, string | number>;
|
|
27
|
+
expires?: number | Date;
|
|
28
|
+
maxAge?: number;
|
|
29
|
+
domain?: string;
|
|
30
|
+
path?: string;
|
|
31
|
+
secure?: boolean;
|
|
32
|
+
httpOnly?: boolean;
|
|
33
|
+
sameSite?: 'lax' | 'strict' | 'none';
|
|
34
|
+
}): Promise<void> {
|
|
35
|
+
const cookieStore = await cookies();
|
|
36
|
+
const name = replacePlaceholders(storageKey.encrypted ? storageKey.secretName : storageKey.name, options?.replacements || {});
|
|
37
|
+
|
|
38
|
+
let finalValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
39
|
+
if (storageKey.encrypted) {
|
|
40
|
+
finalValue = encrypt(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
cookieStore.set(name, finalValue, {
|
|
44
|
+
expires: options?.expires,
|
|
45
|
+
maxAge: options?.maxAge,
|
|
46
|
+
domain: options?.domain,
|
|
47
|
+
path: options?.path || '/',
|
|
48
|
+
secure: options?.secure ?? process.env.NODE_ENV === 'production',
|
|
49
|
+
httpOnly: options?.httpOnly ?? true,
|
|
50
|
+
sameSite: options?.sameSite || 'lax',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function clearCookie(storageKey: IStorageOptions, options?: { replacements?: Record<string, string | number> }): Promise<void> {
|
|
55
|
+
const cookieStore = await cookies();
|
|
56
|
+
const name = replacePlaceholders(storageKey.encrypted ? storageKey.secretName : storageKey.name, options?.replacements || {});
|
|
57
|
+
cookieStore.delete(name);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function clearAllCookies(): Promise<void> {
|
|
61
|
+
const cookieStore = await cookies();
|
|
62
|
+
const all = cookieStore.getAll();
|
|
63
|
+
all.forEach(c => cookieStore.delete(c.name));
|
|
64
|
+
}
|
|
@@ -0,0 +1,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 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
|
+
}
|
|
@@ -0,0 +1,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
|
+
localStorage.clear();
|
|
48
|
+
}
|
|
49
|
+
}
|