@caseparts-org/casecore 0.0.3 → 0.0.5
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 +60 -60
- package/dist/environment.d.ts +3 -0
- package/dist/environment.js +2 -0
- package/dist/src/App.d.ts +3 -0
- package/dist/src/App.js +46 -0
- package/dist/src/lib/authentication/AuthContext.d.ts +34 -0
- package/dist/src/lib/authentication/AuthContext.js +252 -0
- package/dist/src/lib/authentication/AuthContext.test.js +214 -0
- package/dist/src/lib/authentication/AuthTypes.d.ts +82 -0
- package/dist/src/lib/authentication/AuthTypes.js +1 -0
- package/dist/src/lib/hooks/useLocalStorage.d.ts +14 -0
- package/dist/src/lib/hooks/useLocalStorage.js +41 -0
- package/dist/src/lib/hooks/useMicroFlex.d.ts +28 -0
- package/dist/src/lib/hooks/useMicroFlex.js +138 -0
- package/dist/src/lib/index.d.ts +6 -0
- package/dist/src/lib/index.js +6 -0
- package/dist/src/lib/utils/ClaimsUtils.d.ts +16 -0
- package/dist/src/lib/utils/ClaimsUtils.js +76 -0
- package/dist/src/lib/utils/SessionUtils.d.ts +7 -0
- package/dist/src/lib/utils/SessionUtils.js +18 -0
- package/dist/src/local/impersonate/Impersonate.d.ts +1 -0
- package/dist/src/local/impersonate/Impersonate.js +62 -0
- package/dist/src/local/layout/Layout.d.ts +4 -0
- package/dist/src/local/layout/Layout.js +33 -0
- package/dist/src/local/login/Login.d.ts +1 -0
- package/dist/src/local/login/Login.js +53 -0
- package/dist/src/local/microflex/MicroFlex.d.ts +1 -0
- package/dist/src/local/microflex/MicroFlex.js +47 -0
- package/dist/src/local/util/AppSettings.d.ts +109 -0
- package/dist/src/local/util/AppSettings.js +138 -0
- package/dist/src/main.d.ts +1 -0
- package/dist/src/main.js +6 -0
- package/package.json +54 -53
- package/dist/authentication/AuthContext.d.ts +0 -14
- package/dist/authentication/AuthContext.helpers.d.ts +0 -0
- package/dist/authentication/AuthContext.helpers.js +0 -1
- package/dist/authentication/AuthContext.js +0 -21
- package/dist/authentication/AuthContext.test.js +0 -29
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -1
- /package/dist/{authentication → src/lib/authentication}/AuthContext.test.d.ts +0 -0
- /package/dist/{test → src/lib/test}/setup.d.ts +0 -0
- /package/dist/{test → src/lib/test}/setup.js +0 -0
@@ -0,0 +1,214 @@
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
3
|
+
import { render, screen, waitFor, act } from '@testing-library/react';
|
4
|
+
import { useAuthContext, default as AuthProvider } from './AuthContext';
|
5
|
+
import { buildClaimsFromPayload } from '../utils/ClaimsUtils';
|
6
|
+
// Dummy data for API responses
|
7
|
+
const dummyEmployee = {
|
8
|
+
UserId: 'emp1',
|
9
|
+
UserName: 'employee@example.com',
|
10
|
+
NickName: 'Emp',
|
11
|
+
Branch: 'HQ',
|
12
|
+
Rights: 'VIEW,EDIT',
|
13
|
+
};
|
14
|
+
const dummyCustomer = {
|
15
|
+
UserName: 'customer@example.com',
|
16
|
+
UserId: 'cust1',
|
17
|
+
CustKey: 123,
|
18
|
+
CustId: 'C123',
|
19
|
+
CustName: 'Test Customer',
|
20
|
+
CustClass: 'Dealer',
|
21
|
+
FirstName: 'John',
|
22
|
+
LastName: 'Doe',
|
23
|
+
Address: '123 Main St',
|
24
|
+
City: 'Metropolis',
|
25
|
+
State: 'NY',
|
26
|
+
ZipCode: '10001',
|
27
|
+
Phone: '555-1234',
|
28
|
+
Hq: 'HQ',
|
29
|
+
HqCustKey: '456',
|
30
|
+
PaymentTerms: 'Net30',
|
31
|
+
LastLoginDate: '2024-01-01',
|
32
|
+
Rights: 'BUY,SELL',
|
33
|
+
PendingInvites: ['invite1'],
|
34
|
+
PrimaryAdmin: 'admin1',
|
35
|
+
};
|
36
|
+
const dummyJwtPayload = {
|
37
|
+
Time: 'now',
|
38
|
+
IpAddress: '127.0.0.1',
|
39
|
+
Origin: 'test',
|
40
|
+
UserType: 'Employee',
|
41
|
+
Customer: JSON.stringify(dummyCustomer),
|
42
|
+
Employee: JSON.stringify(dummyEmployee),
|
43
|
+
exp: 9999999999,
|
44
|
+
iss: 'issuer',
|
45
|
+
};
|
46
|
+
const dummyJwt = 'dummy.jwt.token';
|
47
|
+
const guestDefaultClaims = {
|
48
|
+
Time: 'now',
|
49
|
+
IpAddress: '127.0.0.1',
|
50
|
+
Origin: 'test',
|
51
|
+
UserType: "Guest",
|
52
|
+
Customer: null,
|
53
|
+
Employee: null,
|
54
|
+
exp: 9999999999,
|
55
|
+
iss: 'issuer',
|
56
|
+
};
|
57
|
+
const guestJwtPayload = guestDefaultClaims;
|
58
|
+
const guestJwt = 'guest.jwt.token';
|
59
|
+
// Mock jwtDecode to just return our dummy payload
|
60
|
+
vi.mock('jwt-decode', () => ({
|
61
|
+
jwtDecode: (jwt) => {
|
62
|
+
if (jwt === dummyJwt) {
|
63
|
+
return dummyJwtPayload;
|
64
|
+
}
|
65
|
+
else {
|
66
|
+
return guestJwtPayload;
|
67
|
+
}
|
68
|
+
}
|
69
|
+
}));
|
70
|
+
global.fetch = vi.fn();
|
71
|
+
const urls = {
|
72
|
+
Login: '/api/login',
|
73
|
+
Logout: '/api/logout',
|
74
|
+
Impersonate: (email) => `/api/impersonate/${email}`,
|
75
|
+
};
|
76
|
+
const apiKey = 'test-api-key';
|
77
|
+
const sessionClientId = 'test-session-id';
|
78
|
+
function TestConsumer() {
|
79
|
+
const ctx = useAuthContext();
|
80
|
+
if (!ctx)
|
81
|
+
return _jsx("div", { children: "no context" });
|
82
|
+
return (_jsxs("div", { children: [_jsx("div", { "data-testid": "initialized", children: ctx.initialized }), _jsx("div", { "data-testid": "email", children: ctx.email }), _jsx("div", { "data-testid": "userType", children: ctx.userType }), _jsx("div", { "data-testid": "claims", children: JSON.stringify(ctx.claims) }), _jsx("button", { onClick: () => ctx.login('test@example.com', 'pw'), children: "Login" }), _jsx("button", { onClick: () => ctx.logout(), children: "Logout" }), _jsx("button", { onClick: () => ctx.impersonate('imp@example.com'), children: "Impersonate" })] }));
|
83
|
+
}
|
84
|
+
describe('AuthProvider', () => {
|
85
|
+
// Setup mock API endpoints
|
86
|
+
beforeEach(() => {
|
87
|
+
vi.clearAllMocks();
|
88
|
+
// Default fetch mock: login returns a dummy JWT string
|
89
|
+
global.fetch.mockImplementation((url, opts) => {
|
90
|
+
if (url.includes('/api/login')) {
|
91
|
+
// If no body or body does not include UserName/Password, return guest JWT
|
92
|
+
if (!opts?.body) {
|
93
|
+
return Promise.resolve({
|
94
|
+
status: 200,
|
95
|
+
text: () => Promise.resolve('"' + guestJwt + '"'),
|
96
|
+
});
|
97
|
+
}
|
98
|
+
try {
|
99
|
+
const body = JSON.parse(opts.body);
|
100
|
+
if (!body.UserName || !body.Password) {
|
101
|
+
return Promise.resolve({
|
102
|
+
status: 200,
|
103
|
+
text: () => Promise.resolve('"' + guestJwt + '"'),
|
104
|
+
});
|
105
|
+
}
|
106
|
+
}
|
107
|
+
catch {
|
108
|
+
// fallback to guest
|
109
|
+
return Promise.resolve({
|
110
|
+
status: 200,
|
111
|
+
text: () => Promise.resolve('"' + guestJwt + '"'),
|
112
|
+
});
|
113
|
+
}
|
114
|
+
// Otherwise, return employee/customer JWT
|
115
|
+
return Promise.resolve({
|
116
|
+
status: 200,
|
117
|
+
text: () => Promise.resolve('"' + dummyJwt + '"'),
|
118
|
+
});
|
119
|
+
}
|
120
|
+
if (url.includes('/api/impersonate')) {
|
121
|
+
return Promise.resolve({
|
122
|
+
status: 200,
|
123
|
+
text: () => Promise.resolve('"' + dummyJwt + '"'),
|
124
|
+
});
|
125
|
+
}
|
126
|
+
if (url.includes('/api/logout')) {
|
127
|
+
return Promise.resolve({
|
128
|
+
status: 200,
|
129
|
+
text: () => Promise.resolve(''),
|
130
|
+
});
|
131
|
+
}
|
132
|
+
return Promise.resolve({ status: 404, text: () => Promise.resolve('Not found') });
|
133
|
+
});
|
134
|
+
// Clear localStorage
|
135
|
+
window.localStorage.clear();
|
136
|
+
});
|
137
|
+
afterEach(() => {
|
138
|
+
vi.clearAllMocks();
|
139
|
+
window.localStorage.clear();
|
140
|
+
});
|
141
|
+
// Tests
|
142
|
+
it('calls login on mount (useEffect) and sets state', async () => {
|
143
|
+
await act(async () => {
|
144
|
+
render(_jsx(AuthProvider, { urls: urls, apiKey: apiKey, sessionClientId: sessionClientId, children: _jsx(TestConsumer, {}) }));
|
145
|
+
});
|
146
|
+
await waitFor(() => {
|
147
|
+
expect(screen.getByTestId('initialized').textContent).toBe('OK');
|
148
|
+
expect(screen.getByTestId('email').textContent).toBe('');
|
149
|
+
expect(screen.getByTestId('userType').textContent).toBe('Guest');
|
150
|
+
expect(screen.getByTestId('claims').textContent).toContain('');
|
151
|
+
});
|
152
|
+
});
|
153
|
+
it('loads token from localStorage and sets state on mount', async () => {
|
154
|
+
// The key used by useLocalStorage (with CPC. prefix)
|
155
|
+
window.localStorage.setItem('CPC.token', JSON.stringify(dummyJwt));
|
156
|
+
await act(async () => {
|
157
|
+
render(_jsx(AuthProvider, { urls: urls, apiKey: apiKey, sessionClientId: sessionClientId, children: _jsx(TestConsumer, {}) }));
|
158
|
+
});
|
159
|
+
await waitFor(() => {
|
160
|
+
expect(screen.getByTestId('initialized').textContent).toBe('OK');
|
161
|
+
expect(screen.getByTestId('email').textContent).toBe('customer@example.com');
|
162
|
+
expect(screen.getByTestId('userType').textContent).toBe('Employee');
|
163
|
+
expect(screen.getByTestId('claims').textContent).toContain('customer@example.com');
|
164
|
+
});
|
165
|
+
});
|
166
|
+
it('login() sets state with new credentials', async () => {
|
167
|
+
render(_jsx(AuthProvider, { urls: urls, apiKey: apiKey, sessionClientId: sessionClientId, children: _jsx(TestConsumer, {}) }));
|
168
|
+
await act(async () => {
|
169
|
+
screen.getByText('Login').click();
|
170
|
+
});
|
171
|
+
await waitFor(() => {
|
172
|
+
expect(screen.getByTestId('initialized').textContent).toBe('OK');
|
173
|
+
expect(screen.getByTestId('email').textContent).toBe('customer@example.com');
|
174
|
+
});
|
175
|
+
});
|
176
|
+
it('impersonate() sets state with impersonated user', async () => {
|
177
|
+
render(_jsx(AuthProvider, { urls: urls, apiKey: apiKey, sessionClientId: sessionClientId, children: _jsx(TestConsumer, {}) }));
|
178
|
+
await act(async () => {
|
179
|
+
screen.getByText('Impersonate').click();
|
180
|
+
});
|
181
|
+
await waitFor(() => {
|
182
|
+
expect(screen.getByTestId('initialized').textContent).toBe('OK');
|
183
|
+
expect(screen.getByTestId('email').textContent).toBe('customer@example.com');
|
184
|
+
});
|
185
|
+
});
|
186
|
+
it('logout() resets state to guest', async () => {
|
187
|
+
render(_jsx(AuthProvider, { urls: urls, apiKey: apiKey, sessionClientId: sessionClientId, children: _jsx(TestConsumer, {}) }));
|
188
|
+
// First, login
|
189
|
+
await act(async () => {
|
190
|
+
screen.getByText('Login').click();
|
191
|
+
});
|
192
|
+
await waitFor(() => {
|
193
|
+
expect(screen.getByTestId('userType').textContent).toBe('Employee');
|
194
|
+
});
|
195
|
+
// Now, logout
|
196
|
+
await act(async () => {
|
197
|
+
screen.getByText('Logout').click();
|
198
|
+
});
|
199
|
+
await waitFor(async () => {
|
200
|
+
const userTypeDiv = await screen.findByTestId('userType');
|
201
|
+
expect(userTypeDiv.textContent).toBe('Guest');
|
202
|
+
});
|
203
|
+
});
|
204
|
+
});
|
205
|
+
describe('buildClaimsFromPayload', () => {
|
206
|
+
it('parses payload and normalizes claims', () => {
|
207
|
+
const claims = buildClaimsFromPayload(dummyJwtPayload);
|
208
|
+
expect(claims.Customer?.UserName).toBe('customer@example.com');
|
209
|
+
expect(claims.Employee?.UserName).toBe('employee@example.com');
|
210
|
+
expect(claims.UserType).toBe('Employee');
|
211
|
+
expect(Array.isArray(claims.Customer?.Rights)).toBe(true);
|
212
|
+
expect(Array.isArray(claims.Employee?.Rights)).toBe(true);
|
213
|
+
});
|
214
|
+
});
|
@@ -0,0 +1,82 @@
|
|
1
|
+
export type EmployeeResponse = {
|
2
|
+
UserId: string;
|
3
|
+
UserName: string;
|
4
|
+
NickName: string;
|
5
|
+
Branch: string;
|
6
|
+
Rights: string;
|
7
|
+
};
|
8
|
+
export type CustomerResponse = {
|
9
|
+
UserName: string;
|
10
|
+
UserId: string;
|
11
|
+
CustKey: number;
|
12
|
+
CustId: string;
|
13
|
+
CustName: string;
|
14
|
+
CustClass: string;
|
15
|
+
FirstName: string;
|
16
|
+
LastName: string;
|
17
|
+
Address: string;
|
18
|
+
City: string;
|
19
|
+
State: string;
|
20
|
+
ZipCode: string;
|
21
|
+
Phone: string;
|
22
|
+
HQ: string;
|
23
|
+
HQCustKey: string;
|
24
|
+
PaymentTerms: string;
|
25
|
+
LastLoginDate: string | null;
|
26
|
+
Rights: string;
|
27
|
+
PendingInvites: Invite[];
|
28
|
+
PrimaryAdmin: string;
|
29
|
+
};
|
30
|
+
export type InviteType = "NewAcctPrimary" | "Standard" | "Primary" | "LinkedStandard" | "LinkedPrimary";
|
31
|
+
export type Invite = {
|
32
|
+
Id: string;
|
33
|
+
SenderEmail: string;
|
34
|
+
CustKey: number;
|
35
|
+
CustId: string;
|
36
|
+
CustName: string;
|
37
|
+
TargetEmail: string;
|
38
|
+
TargetUserId: string;
|
39
|
+
InviteType: InviteType | string;
|
40
|
+
Rights: string;
|
41
|
+
Status: string;
|
42
|
+
CreatedDate: string;
|
43
|
+
};
|
44
|
+
export type EmployeeClaims = {
|
45
|
+
UserId: string;
|
46
|
+
UserName: string;
|
47
|
+
NickName: string;
|
48
|
+
Branch: string | null;
|
49
|
+
Rights: string[];
|
50
|
+
};
|
51
|
+
export type CustomerClaims = {
|
52
|
+
UserName: string;
|
53
|
+
UserId: string;
|
54
|
+
CustKey: number;
|
55
|
+
CustId: string;
|
56
|
+
CustName: string;
|
57
|
+
CustClass: string;
|
58
|
+
FirstName: string;
|
59
|
+
LastName: string;
|
60
|
+
Address: string;
|
61
|
+
City: string;
|
62
|
+
State: string;
|
63
|
+
ZipCode: string;
|
64
|
+
Phone: string;
|
65
|
+
HQ: string | null;
|
66
|
+
HQCustKey: number;
|
67
|
+
PaymentTerms: string;
|
68
|
+
LastLoginDate: string;
|
69
|
+
Rights: string[];
|
70
|
+
PendingInvites: unknown[];
|
71
|
+
PrimaryAdmin: unknown | null;
|
72
|
+
};
|
73
|
+
export type Claims = {
|
74
|
+
Time: string;
|
75
|
+
IpAddress: string;
|
76
|
+
Origin: string;
|
77
|
+
UserType: "Guest" | "Employee" | "LinkedCustomer" | string;
|
78
|
+
Customer: CustomerClaims | null;
|
79
|
+
Employee: EmployeeClaims | null;
|
80
|
+
exp: number;
|
81
|
+
iss: string;
|
82
|
+
};
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -0,0 +1,14 @@
|
|
1
|
+
/**
|
2
|
+
* React hook for managing a value in localStorage with a CPC-prefixed key.
|
3
|
+
*
|
4
|
+
* The key is automatically prefixed with "CPC." if not already present (case-insensitive).
|
5
|
+
*
|
6
|
+
* @template T The type of the value to store.
|
7
|
+
* @param {string} key - The key to use in localStorage. Will be prefixed with "CPC." if not already.
|
8
|
+
* @param {T} initialValue - The initial value to use if no value is found in localStorage.
|
9
|
+
* @returns {[T, (value?: T) => void]} A tuple containing the current value and a setter function.
|
10
|
+
*
|
11
|
+
* The setter function updates both state and localStorage. Passing `undefined` removes the item from localStorage.
|
12
|
+
* Passing `null` resets the value to the initial value.
|
13
|
+
*/
|
14
|
+
export default function useLocalStorage<T>(key: string, initialValue: T): [T, (_value?: T) => void];
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import { useState } from "react";
|
2
|
+
function getPrefixedKey(key) {
|
3
|
+
return key.toLowerCase().startsWith("cpc.") ? key : "CPC." + key;
|
4
|
+
}
|
5
|
+
/**
|
6
|
+
* React hook for managing a value in localStorage with a CPC-prefixed key.
|
7
|
+
*
|
8
|
+
* The key is automatically prefixed with "CPC." if not already present (case-insensitive).
|
9
|
+
*
|
10
|
+
* @template T The type of the value to store.
|
11
|
+
* @param {string} key - The key to use in localStorage. Will be prefixed with "CPC." if not already.
|
12
|
+
* @param {T} initialValue - The initial value to use if no value is found in localStorage.
|
13
|
+
* @returns {[T, (value?: T) => void]} A tuple containing the current value and a setter function.
|
14
|
+
*
|
15
|
+
* The setter function updates both state and localStorage. Passing `undefined` removes the item from localStorage.
|
16
|
+
* Passing `null` resets the value to the initial value.
|
17
|
+
*/
|
18
|
+
export default function useLocalStorage(key, initialValue) {
|
19
|
+
const fullKey = getPrefixedKey(key);
|
20
|
+
const getStoredValue = () => {
|
21
|
+
try {
|
22
|
+
const item = window.localStorage.getItem(fullKey);
|
23
|
+
return item ? JSON.parse(item) : initialValue;
|
24
|
+
}
|
25
|
+
catch {
|
26
|
+
return initialValue;
|
27
|
+
}
|
28
|
+
};
|
29
|
+
const [storedValue, setStoredValue] = useState(getStoredValue);
|
30
|
+
const setValue = (value) => {
|
31
|
+
const denullified = value === null ? initialValue : value;
|
32
|
+
setStoredValue(denullified);
|
33
|
+
if (value === undefined) {
|
34
|
+
window.localStorage.removeItem(fullKey);
|
35
|
+
}
|
36
|
+
else {
|
37
|
+
window.localStorage.setItem(fullKey, JSON.stringify(denullified));
|
38
|
+
}
|
39
|
+
};
|
40
|
+
return [storedValue, setValue];
|
41
|
+
}
|
@@ -0,0 +1,28 @@
|
|
1
|
+
type MicroFlexFieldSelectors = {
|
2
|
+
number: string;
|
3
|
+
securityCode: string;
|
4
|
+
};
|
5
|
+
type MicroFlexOptions = {
|
6
|
+
initializeForm: boolean;
|
7
|
+
apiURL: string;
|
8
|
+
fetchFn: (_url: string, _options?: RequestInit) => Promise<Response>;
|
9
|
+
fieldSelectors?: MicroFlexFieldSelectors;
|
10
|
+
styles?: Record<string, any>;
|
11
|
+
scriptUrl?: string;
|
12
|
+
setupInterval?: number;
|
13
|
+
onError?: (_error: unknown) => void;
|
14
|
+
};
|
15
|
+
type MicroFlexReturn = {
|
16
|
+
validate: () => Promise<string | undefined>;
|
17
|
+
cardToken: string | null;
|
18
|
+
cardError: string | null;
|
19
|
+
setupError: unknown;
|
20
|
+
autoCompleteData: unknown;
|
21
|
+
};
|
22
|
+
declare global {
|
23
|
+
interface Window {
|
24
|
+
Flex?: any;
|
25
|
+
}
|
26
|
+
}
|
27
|
+
export default function useMicroFlex({ initializeForm, apiURL, fetchFn, fieldSelectors, styles, scriptUrl, setupInterval, onError, }: MicroFlexOptions): MicroFlexReturn;
|
28
|
+
export {};
|
@@ -0,0 +1,138 @@
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
2
|
+
const defaultStyles = {
|
3
|
+
input: {
|
4
|
+
"font-size": "1rem",
|
5
|
+
"font-family": "Roboto, helvetica, tahoma, calibri, sans-serif",
|
6
|
+
color: "#555",
|
7
|
+
"line-height": "1rem",
|
8
|
+
},
|
9
|
+
":focus": { color: "blue" },
|
10
|
+
":disabled": { cursor: "not-allowed" },
|
11
|
+
valid: { color: "#3C763D" },
|
12
|
+
invalid: { color: "#A94442" },
|
13
|
+
};
|
14
|
+
const defaultFieldSelectors = { number: "#number", securityCode: "#securityCode" };
|
15
|
+
const defaultScriptUrl = "https://flex.cybersource.com/cybersource/assets/microform/0.11/flex-microform.min.js";
|
16
|
+
export default function useMicroFlex({ initializeForm, apiURL, fetchFn, fieldSelectors = defaultFieldSelectors, styles = defaultStyles, scriptUrl = defaultScriptUrl, setupInterval = 15 * 60 * 1000, onError, }) {
|
17
|
+
const [error, setError] = useState(null);
|
18
|
+
const [autoCompleteData, setAutoCompleteData] = useState(null);
|
19
|
+
const [setupError, setSetupError] = useState(null);
|
20
|
+
const [token, setToken] = useState(null);
|
21
|
+
const formRef = useRef(null);
|
22
|
+
useEffect(() => {
|
23
|
+
if (!initializeForm)
|
24
|
+
return;
|
25
|
+
const scriptLoaded = !!window.Flex;
|
26
|
+
if (!scriptLoaded) {
|
27
|
+
const script = document.createElement("script");
|
28
|
+
script.src = scriptUrl;
|
29
|
+
script.async = true;
|
30
|
+
script.onload = () => {
|
31
|
+
setupMicroform();
|
32
|
+
};
|
33
|
+
document.body.appendChild(script);
|
34
|
+
}
|
35
|
+
try {
|
36
|
+
setupMicroform();
|
37
|
+
}
|
38
|
+
catch {
|
39
|
+
// Script might not be loaded yet
|
40
|
+
}
|
41
|
+
const timerId = setInterval(() => setupMicroform(), setupInterval);
|
42
|
+
return () => {
|
43
|
+
clearInterval(timerId);
|
44
|
+
};
|
45
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
46
|
+
}, [initializeForm, apiURL, scriptUrl]);
|
47
|
+
async function setupMicroform() {
|
48
|
+
if (!window.Flex)
|
49
|
+
return;
|
50
|
+
setToken(null);
|
51
|
+
setError(null);
|
52
|
+
let jwt;
|
53
|
+
try {
|
54
|
+
jwt = await fetchFn(apiURL, { method: "GET" })
|
55
|
+
.then((resp) => resp.text())
|
56
|
+
.catch((err) => {
|
57
|
+
setSetupError(err);
|
58
|
+
if (onError)
|
59
|
+
onError(err);
|
60
|
+
return undefined;
|
61
|
+
});
|
62
|
+
}
|
63
|
+
catch (err) {
|
64
|
+
setSetupError(err);
|
65
|
+
if (onError)
|
66
|
+
onError(err);
|
67
|
+
return;
|
68
|
+
}
|
69
|
+
if (!jwt)
|
70
|
+
return;
|
71
|
+
try {
|
72
|
+
const flex = new window.Flex(jwt);
|
73
|
+
const microform = flex.microform({ styles });
|
74
|
+
formRef.current = microform;
|
75
|
+
const number = microform.createField("number");
|
76
|
+
const securityCode = microform.createField("securityCode");
|
77
|
+
number.on("autocomplete", function (data) {
|
78
|
+
setAutoCompleteData(data);
|
79
|
+
setToken(null);
|
80
|
+
});
|
81
|
+
securityCode.on("autocomplete", function (data) {
|
82
|
+
setAutoCompleteData(data);
|
83
|
+
setToken(null);
|
84
|
+
});
|
85
|
+
number.on("change", () => {
|
86
|
+
setToken(null);
|
87
|
+
setError(null);
|
88
|
+
});
|
89
|
+
securityCode.on("change", () => {
|
90
|
+
setToken(null);
|
91
|
+
setError(null);
|
92
|
+
});
|
93
|
+
number.load(fieldSelectors.number);
|
94
|
+
securityCode.load(fieldSelectors.securityCode);
|
95
|
+
}
|
96
|
+
catch (error) {
|
97
|
+
setSetupError(error);
|
98
|
+
if (onError)
|
99
|
+
onError(error);
|
100
|
+
}
|
101
|
+
}
|
102
|
+
function validate() {
|
103
|
+
return new Promise((resolve, reject) => {
|
104
|
+
if (!formRef.current) {
|
105
|
+
const err = new Error("Form reference is not available.");
|
106
|
+
if (onError)
|
107
|
+
onError(err);
|
108
|
+
return reject(err);
|
109
|
+
}
|
110
|
+
formRef.current.createToken({}, function (error, token) {
|
111
|
+
let errorMessage = error?.message;
|
112
|
+
if (error && error.status === 429) {
|
113
|
+
errorMessage = "Credit card could not be validated. Please refresh and try again.";
|
114
|
+
}
|
115
|
+
if (error && error.status === 400) {
|
116
|
+
errorMessage = "Invalid credit card. Please double-check details.";
|
117
|
+
}
|
118
|
+
setError(errorMessage);
|
119
|
+
setToken(token);
|
120
|
+
if (error) {
|
121
|
+
if (onError)
|
122
|
+
onError(error);
|
123
|
+
reject(new Error(errorMessage));
|
124
|
+
}
|
125
|
+
else {
|
126
|
+
resolve(token);
|
127
|
+
}
|
128
|
+
});
|
129
|
+
});
|
130
|
+
}
|
131
|
+
return {
|
132
|
+
validate,
|
133
|
+
cardToken: token,
|
134
|
+
cardError: error,
|
135
|
+
setupError,
|
136
|
+
autoCompleteData,
|
137
|
+
};
|
138
|
+
}
|
@@ -0,0 +1,6 @@
|
|
1
|
+
export type { AuthUrls, AuthContextType, AuthProviderProps, Credentials } from './authentication/AuthContext';
|
2
|
+
export { default as AuthProvider, useAuthContext } from './authentication/AuthContext';
|
3
|
+
export type { EmployeeResponse, CustomerResponse, InviteType, Invite, EmployeeClaims, CustomerClaims, Claims } from './authentication/AuthTypes';
|
4
|
+
export { default as useLocalStorage } from './hooks/useLocalStorage';
|
5
|
+
export { getSessionId } from './utils/SessionUtils';
|
6
|
+
export { buildClaimsFromPayload } from './utils/ClaimsUtils';
|
@@ -0,0 +1,6 @@
|
|
1
|
+
// Export AuthProvider as default and useAuthContext hook
|
2
|
+
export { default as AuthProvider, useAuthContext } from './authentication/AuthContext';
|
3
|
+
// Export utility hooks and functions
|
4
|
+
export { default as useLocalStorage } from './hooks/useLocalStorage';
|
5
|
+
export { getSessionId } from './utils/SessionUtils';
|
6
|
+
export { buildClaimsFromPayload } from './utils/ClaimsUtils';
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import { Claims } from "../authentication/AuthTypes";
|
2
|
+
/**
|
3
|
+
* Converts a decoded JWT payload into a normalized Claims object for authentication.
|
4
|
+
*
|
5
|
+
* This function parses the nested Customer and Employee fields (if present) from the payload,
|
6
|
+
* normalizes their structure (including splitting comma-separated rights into arrays),
|
7
|
+
* and returns a Claims object suitable for use in authentication and authorization logic.
|
8
|
+
*
|
9
|
+
* - If the payload contains stringified JSON for Customer or Employee, they are parsed and normalized.
|
10
|
+
* - Rights fields are always returned as arrays of strings.
|
11
|
+
* - Handles missing or malformed fields gracefully, returning nulls or empty arrays as appropriate.
|
12
|
+
*
|
13
|
+
* @param payload - The decoded JWT payload as a plain object.
|
14
|
+
* @returns {Claims} The normalized Claims object with Customer and Employee claims, or nulls if not present.
|
15
|
+
*/
|
16
|
+
export declare function buildClaimsFromPayload(payload: Record<string, any>): Claims;
|
@@ -0,0 +1,76 @@
|
|
1
|
+
/**
|
2
|
+
* Converts a decoded JWT payload into a normalized Claims object for authentication.
|
3
|
+
*
|
4
|
+
* This function parses the nested Customer and Employee fields (if present) from the payload,
|
5
|
+
* normalizes their structure (including splitting comma-separated rights into arrays),
|
6
|
+
* and returns a Claims object suitable for use in authentication and authorization logic.
|
7
|
+
*
|
8
|
+
* - If the payload contains stringified JSON for Customer or Employee, they are parsed and normalized.
|
9
|
+
* - Rights fields are always returned as arrays of strings.
|
10
|
+
* - Handles missing or malformed fields gracefully, returning nulls or empty arrays as appropriate.
|
11
|
+
*
|
12
|
+
* @param payload - The decoded JWT payload as a plain object.
|
13
|
+
* @returns {Claims} The normalized Claims object with Customer and Employee claims, or nulls if not present.
|
14
|
+
*/
|
15
|
+
export function buildClaimsFromPayload(payload) {
|
16
|
+
// Parse nested Customer and Employee using API response types
|
17
|
+
function parseNested(obj, name) {
|
18
|
+
if (obj && typeof obj === "object" && typeof obj[name] === "string" && obj[name].length > 0) {
|
19
|
+
return JSON.parse(obj[name]);
|
20
|
+
}
|
21
|
+
return "";
|
22
|
+
}
|
23
|
+
const customerRaw = parseNested(payload, "Customer");
|
24
|
+
const employeeRaw = parseNested(payload, "Employee");
|
25
|
+
// Convert EmployeeResponse to EmployeeClaims (normalize Rights)
|
26
|
+
let employeeClaims = null;
|
27
|
+
if (employeeRaw && typeof employeeRaw === "object") {
|
28
|
+
employeeClaims = {
|
29
|
+
UserId: employeeRaw.UserId,
|
30
|
+
UserName: employeeRaw.UserName,
|
31
|
+
NickName: employeeRaw.NickName,
|
32
|
+
Branch: employeeRaw.Branch ?? null,
|
33
|
+
Rights: typeof employeeRaw.Rights === "string"
|
34
|
+
? employeeRaw.Rights.split(",").map((s) => s.trim())
|
35
|
+
: [],
|
36
|
+
};
|
37
|
+
}
|
38
|
+
// Convert CustomerResponse to CustomerClaims (normalize Rights, PendingInvites, etc.)
|
39
|
+
let customerClaims = null;
|
40
|
+
if (customerRaw && typeof customerRaw === "object") {
|
41
|
+
customerClaims = {
|
42
|
+
UserName: customerRaw.UserName,
|
43
|
+
UserId: customerRaw.UserId,
|
44
|
+
CustKey: customerRaw.CustKey,
|
45
|
+
CustId: customerRaw.CustId,
|
46
|
+
CustName: customerRaw.CustName,
|
47
|
+
CustClass: customerRaw.CustClass,
|
48
|
+
FirstName: customerRaw.FirstName,
|
49
|
+
LastName: customerRaw.LastName,
|
50
|
+
Address: customerRaw.Address,
|
51
|
+
City: customerRaw.City,
|
52
|
+
State: customerRaw.State,
|
53
|
+
ZipCode: customerRaw.ZipCode,
|
54
|
+
Phone: customerRaw.Phone,
|
55
|
+
HQ: customerRaw.HQ ?? null,
|
56
|
+
HQCustKey: typeof customerRaw.HQCustKey === "string" ? parseInt(customerRaw.HQCustKey, 10) : customerRaw.HQCustKey,
|
57
|
+
PaymentTerms: customerRaw.PaymentTerms,
|
58
|
+
LastLoginDate: customerRaw.LastLoginDate ?? "",
|
59
|
+
Rights: typeof customerRaw.Rights === "string"
|
60
|
+
? customerRaw.Rights.split(",").map((s) => s.trim())
|
61
|
+
: [],
|
62
|
+
PendingInvites: customerRaw.PendingInvites ?? [],
|
63
|
+
PrimaryAdmin: customerRaw.PrimaryAdmin ?? null,
|
64
|
+
};
|
65
|
+
}
|
66
|
+
return {
|
67
|
+
Time: String(payload.Time ?? ""),
|
68
|
+
IpAddress: String(payload.IpAddress ?? ""),
|
69
|
+
Origin: String(payload.Origin ?? ""),
|
70
|
+
UserType: String(payload.UserType ?? "Guest"),
|
71
|
+
Customer: customerClaims,
|
72
|
+
Employee: employeeClaims,
|
73
|
+
exp: Number(payload.exp ?? 0),
|
74
|
+
iss: String(payload.iss ?? ""),
|
75
|
+
};
|
76
|
+
}
|
@@ -0,0 +1,7 @@
|
|
1
|
+
/**
|
2
|
+
* Gets or creates a session ID stored in sessionStorage under a CPC-prefixed key.
|
3
|
+
*
|
4
|
+
* @param {string} clientId - The identifier to inject into the session key (e.g., 'CatBaker2').
|
5
|
+
* @returns {string} The session ID.
|
6
|
+
*/
|
7
|
+
export declare const getSessionId: (clientId: string) => string;
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
2
|
+
/**
|
3
|
+
* Gets or creates a session ID stored in sessionStorage under a CPC-prefixed key.
|
4
|
+
*
|
5
|
+
* @param {string} clientId - The identifier to inject into the session key (e.g., 'CatBaker2').
|
6
|
+
* @returns {string} The session ID.
|
7
|
+
*/
|
8
|
+
export const getSessionId = (clientId) => {
|
9
|
+
const SESSION_ID_KEY = `CPC.${clientId}.SessionId`;
|
10
|
+
// Check if we already have a session ID
|
11
|
+
let sessionId = sessionStorage.getItem(SESSION_ID_KEY);
|
12
|
+
// If not, generate a new one and store it
|
13
|
+
if (!sessionId) {
|
14
|
+
sessionId = uuidv4();
|
15
|
+
sessionStorage.setItem(SESSION_ID_KEY, sessionId);
|
16
|
+
}
|
17
|
+
return sessionId;
|
18
|
+
};
|
@@ -0,0 +1 @@
|
|
1
|
+
export default function Impersonate(): import("react/jsx-runtime").JSX.Element;
|