@caseparts-org/casecore 0.0.3 → 0.0.4

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,14 +1,34 @@
1
- import React from "react";
2
- export interface AuthContextValue {
3
- initialized: string;
4
- login: (_email: string, _password: string) => void;
5
- user: string | null;
6
- }
7
- export declare const AuthContext: React.Context<AuthContextValue | undefined>;
8
- export declare const useAuthContext: () => AuthContextValue;
9
- export interface AuthProviderProps {
10
- children: React.ReactNode;
1
+ import React, { ReactNode } from "react";
2
+ import { Claims } from "./AuthTypes";
3
+ export type AuthUrls = {
4
+ Login: string;
5
+ Logout: string;
6
+ Impersonate: (_email: string) => string;
7
+ };
8
+ export type AuthProviderProps = {
9
+ children: ReactNode;
10
+ urls: AuthUrls;
11
11
  apiKey: string;
12
- loginUrl: string;
13
- }
14
- export declare const AuthProvider: React.FC<AuthProviderProps>;
12
+ sessionClientId: string;
13
+ };
14
+ export type AuthContextType = {
15
+ initialized: string;
16
+ claims: Claims | null;
17
+ email: string;
18
+ discountLevel: string;
19
+ userType: string;
20
+ login: (_email?: string, _password?: string) => Promise<unknown>;
21
+ logout: () => Promise<boolean>;
22
+ impersonate: (_email: string) => Promise<string | null>;
23
+ fetch: (_url: string, _options?: RequestInit) => Promise<Response | null>;
24
+ hasRight: (_right: string) => boolean;
25
+ token: string | null;
26
+ sessionId: string;
27
+ };
28
+ export type Credentials = {
29
+ email: string;
30
+ password: string;
31
+ };
32
+ export declare const AuthContext: React.Context<AuthContextType | undefined>;
33
+ export declare const useAuthContext: () => AuthContextType | undefined;
34
+ export default function AuthProvider({ children, urls, apiKey, sessionClientId }: AuthProviderProps): import("react/jsx-runtime").JSX.Element;
@@ -1,21 +1,252 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createContext, useContext, useState } from "react";
2
+ import React, { useContext, useState, useEffect, createContext } from "react";
3
+ import { jwtDecode } from "jwt-decode";
4
+ import useLocalStorage from "../hooks/useLocalStorage";
5
+ import { getSessionId } from '../utils/SessionUtils';
6
+ import { buildClaimsFromPayload } from "../utils/ClaimsUtils";
3
7
  export const AuthContext = createContext(undefined);
4
8
  export const useAuthContext = () => {
5
- const ctx = useContext(AuthContext);
6
- if (!ctx)
7
- throw new Error("Must be inside AuthProvider");
8
- return ctx;
9
+ return useContext(AuthContext);
9
10
  };
10
- export const AuthProvider = ({ children, apiKey, loginUrl, }) => {
11
- const [initialized] = useState("OK");
12
- const [user, setUser] = useState(null);
11
+ export default function AuthProvider({ children, urls, apiKey, sessionClientId }) {
12
+ const [token, setToken] = useState(null);
13
+ const [initialized, setInitialized] = useState("");
14
+ const [claims, setClaims] = useState(null);
15
+ const [localToken, setLocalToken] = useLocalStorage("token", "");
16
+ const credentialsRef = React.useRef(null);
17
+ const sessionId = getSessionId(sessionClientId);
18
+ useEffect(() => {
19
+ if (token)
20
+ return;
21
+ // Logs in using local storage token if present or cookies. If neither exist, API will return a Guest token.
22
+ login()
23
+ .catch(e => setInitialized(e.message));
24
+ // eslint-disable-next-line
25
+ }, [token]); // Triggers login when setting token in `logout` to obtain a Guest token.
26
+ //#region Fetch extensions
27
+ class FetchError extends Error {
28
+ statusCode;
29
+ constructor(statusCode, message) {
30
+ super(message);
31
+ this.name = "FetchError";
32
+ this.statusCode = statusCode;
33
+ }
34
+ }
35
+ function defaultErrorMessage(status, url) {
36
+ switch (status) {
37
+ case 401: return "You are not authenticated";
38
+ case 403: return "You do not have permission to access this page";
39
+ case 404: return "Page does not exist: " + url;
40
+ default: return "Unexpected Error";
41
+ }
42
+ }
43
+ /**
44
+ * Performs a fetch request and throws a FetchError for HTTP errors (except 500).
45
+ *
46
+ * - Returns the response for successful requests (status < 400) and for status 500.
47
+ * - For other error statuses, reads the response text and throws a FetchError with a normalized message.
48
+ * - Handles network or parsing errors by rethrowing them.
49
+ *
50
+ * @param url - The URL to fetch.
51
+ * @param options - Optional fetch options (method, headers, body, etc).
52
+ * @returns {Promise<Response>} The fetch response if successful.
53
+ * @throws {FetchError} If the response status is an error (except 500).
54
+ */
55
+ const fancyFetch = (url, options) => {
56
+ return fetch(url, options)
57
+ .then(async (res) => {
58
+ if (res.status < 400) {
59
+ return res;
60
+ }
61
+ else if (res.status === 500) {
62
+ return res;
63
+ }
64
+ else {
65
+ const text = await res.text();
66
+ const message = text ? String(text).replace(/['"]+/g, '')
67
+ : res.statusText ? res.statusText
68
+ : defaultErrorMessage(res.status, url);
69
+ throw new FetchError(res.status, message);
70
+ }
71
+ })
72
+ .catch(e => { throw e; });
73
+ };
74
+ /**
75
+ * Performs a fetch request with authentication and automatic retry on 401 errors.
76
+ *
77
+ * - Adds Authorization and X-Session-ID headers if available.
78
+ * - On a 401 response, attempts to re-authenticate by calling login(), updates the Authorization header, and retries the request once.
79
+ * - Throws for other errors or if re-authentication fails.
80
+ *
81
+ * @param url - The URL to fetch.
82
+ * @param options - Optional fetch options (method, headers, body, etc).
83
+ * @returns {Promise<Response>} The fetch response if successful.
84
+ * @throws {FetchError} If the request fails or re-authentication fails.
85
+ */
86
+ const fancyFetchWithRetry = (url, options) => {
87
+ const opts = { ...options };
88
+ opts.method = opts.method || 'GET';
89
+ opts.headers = { ...(opts.headers || {}) };
90
+ if (token)
91
+ opts.headers.Authorization = "Bearer " + token;
92
+ opts.headers['X-Session-ID'] = sessionId;
93
+ opts.credentials = 'include';
94
+ return fancyFetch(url, opts)
95
+ .catch(e => {
96
+ if (e.statusCode === 401) {
97
+ return login()
98
+ .then(fetchTokenResult => {
99
+ opts.headers.Authorization = "Bearer " + fetchTokenResult.token;
100
+ })
101
+ .then(() => fancyFetch(url, opts))
102
+ .catch(loginError => { throw loginError; });
103
+ }
104
+ else {
105
+ throw e;
106
+ }
107
+ });
108
+ };
109
+ //#endregion
110
+ //#region Login/Logout with related helpers
111
+ /**
112
+ * Authenticates the user and returns a token/claims object.
113
+ *
114
+ * The login logic attempts authentication in the following order:
115
+ * 1. If running locally and no email is provided, but credentials are stored in memory, it uses those credentials to fetch a token.
116
+ * 2. If running locally and a JWT exists in local storage (and no email is provided), it uses that token and parses claims from it.
117
+ * 3. Otherwise, it calls fetchToken with the provided email and password (or undefined for guest login).
118
+ *
119
+ * This function is used both for explicit user login and for automatic login (e.g., after logout or on mount) to obtain a guest or user token as needed.
120
+ *
121
+ * @param email - Optional email/username for login. If omitted, attempts to use stored credentials or local storage.
122
+ * @param password - Optional password for login.
123
+ * @returns {Promise<FetchTokenResult>} A promise resolving to the token and claims.
124
+ */
13
125
  const login = (email, password) => {
14
- // Use both email and password to avoid unused variable error
15
- console.log(`Logging in with email: ${email} and password: ${password}`);
16
- setUser(`${email}.${password}`);
126
+ // const localTokenJson = window.localStorage.getItem("CPC.token")
127
+ // const localToken = localTokenJson ? JSON.parse(localTokenJson) : null
128
+ if (isLocal() && !email && credentialsRef.current) { // Has logged in using credentials and stored in state
129
+ return fetchToken(credentialsRef.current.email, credentialsRef.current.password);
130
+ }
131
+ if (isLocal() && localToken && !email) { // Local storage jwt exists on localhost
132
+ const claims = assignToken(localToken);
133
+ return Promise.resolve({ token: localToken, claims });
134
+ }
135
+ return fetchToken(email, password);
17
136
  };
18
- console.log(apiKey);
19
- console.log(loginUrl);
20
- return (_jsx(AuthContext.Provider, { value: { initialized, login, user }, children: children }));
21
- };
137
+ /**
138
+ * Logs out the current user and resets authentication state.
139
+ *
140
+ * - Calls the logout API endpoint to invalidate the session on the server.
141
+ * - Clears any stored credentials in memory.
142
+ * - Sets the token to null, which triggers a re-authentication as a guest via useEffect.
143
+ * - Clears claims state.
144
+ *
145
+ * @returns {Promise<boolean>} Resolves to true when logout is complete.
146
+ */
147
+ const logout = () => {
148
+ if (isLocal())
149
+ setLocalToken();
150
+ return fancyFetch(urls.Logout, { method: 'POST', credentials: 'include' })
151
+ .then(() => {
152
+ credentialsRef.current = null;
153
+ setToken(null); // Setting token null triggers useEffect to reauthenticate as an anonymous user
154
+ setClaims(null);
155
+ return true;
156
+ });
157
+ };
158
+ /**
159
+ * Impersonates another user by email, updating authentication state with the new user's claims.
160
+ *
161
+ * - Calls the impersonate API endpoint with the provided email.
162
+ * - Parses and assigns the returned JWT as the current authentication token and claims.
163
+ * - If running locally, stores the new JWT in local storage.
164
+ *
165
+ * @param email - The email address of the user to impersonate.
166
+ * @returns {Promise<string | null>} Resolves to the new JWT string if successful, or null otherwise.
167
+ */
168
+ const impersonate = (email) => {
169
+ return fancyFetchWithRetry(urls.Impersonate(email))
170
+ .then(res => res ? res.text() : null)
171
+ .then(jwt => jwt ? jwt.replace(/['"]+/g, '') : null)
172
+ .then(jwt => {
173
+ assignToken(jwt);
174
+ if (isLocal() && jwt)
175
+ setLocalToken(jwt);
176
+ return jwt;
177
+ });
178
+ };
179
+ function isLocal() {
180
+ const hostname = window.location.hostname;
181
+ return hostname === "localhost" || hostname === "127.0.0.1";
182
+ }
183
+ const assignToken = (jwt) => {
184
+ if (!jwt) {
185
+ throw Error("Cannot parse null");
186
+ }
187
+ const decoded = jwtDecode(jwt);
188
+ const claims = buildClaimsFromPayload(decoded);
189
+ setInitialized("OK");
190
+ setToken(jwt);
191
+ setClaims(claims);
192
+ return claims;
193
+ };
194
+ async function fetchToken(email, password) {
195
+ const options = {
196
+ method: 'POST',
197
+ credentials: 'include',
198
+ headers: {
199
+ 'Accept': 'application/json',
200
+ 'Content-Type': 'application/json',
201
+ 'ApiKey': apiKey
202
+ }
203
+ };
204
+ if (email) {
205
+ options.body = JSON.stringify({
206
+ UserName: email,
207
+ Password: password,
208
+ RememberMe: false
209
+ });
210
+ }
211
+ return fancyFetch(urls.Login, options)
212
+ .then(res => res ? res.text() : null)
213
+ .then(jwt => jwt ? jwt.replace(/['"]+/g, '') : null)
214
+ .then(jwt => {
215
+ const decoded = assignToken(jwt);
216
+ if (isLocal() && email && password) {
217
+ credentialsRef.current = { email, password };
218
+ }
219
+ if (isLocal() && decoded.UserType !== "Guest" && jwt) {
220
+ setLocalToken(jwt); // Store token in Local Storage
221
+ }
222
+ return { token: jwt, claims: decoded };
223
+ });
224
+ }
225
+ //#endregion
226
+ const hasRight = (right) => {
227
+ const claimsType = claims;
228
+ if (claimsType.UserType === "Guest")
229
+ return false;
230
+ if (claimsType.Customer && claimsType.Customer.Rights) {
231
+ return claimsType.Customer.Rights.includes(right);
232
+ }
233
+ if (claimsType.Employee && claimsType.Employee.Rights) {
234
+ return claimsType.Employee.Rights.includes(right);
235
+ }
236
+ return false;
237
+ };
238
+ return (_jsx(AuthContext.Provider, { value: {
239
+ initialized,
240
+ claims,
241
+ email: claims?.Customer?.UserName || claims?.Employee?.UserName || "",
242
+ discountLevel: claims?.Customer?.CustClass || "EndUser",
243
+ userType: claims?.UserType || "",
244
+ login,
245
+ logout,
246
+ impersonate,
247
+ fetch: fancyFetchWithRetry,
248
+ hasRight,
249
+ token,
250
+ sessionId
251
+ }, children: children }));
252
+ }
@@ -1,29 +1,214 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { describe, it, expect } from 'vitest';
3
- import { render, screen } from '@testing-library/react';
4
- import { AuthProvider, useAuthContext } from './AuthContext';
5
- /**
6
- * Dummy component to consume the AuthContext
7
- */
8
- function ShowAuthState() {
9
- const { initialized } = useAuthContext();
10
- return _jsx("span", { "data-testid": "auth-state", children: initialized });
11
- }
12
- describe('AuthContext', () => {
13
- it('provides initialized state to children', () => {
14
- render(_jsx(AuthProvider, { loginUrl: '/login', apiKey: '123', children: _jsx(ShowAuthState, {}) }));
15
- // The initial state should be "OK"
16
- expect(screen.getByTestId('auth-state')).toHaveTextContent('OK');
17
- });
18
- it('throws error if used outside AuthProvider', () => {
19
- // Expect the hook to throw if used outside the provider
20
- // (React Testing Library can't render hooks directly; need to use a component)
21
- // We'll use a function to capture the error:
22
- function CallWithoutProvider() {
23
- useAuthContext();
24
- return null;
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;
25
67
  }
26
- // Wrap in a function to capture the thrown error
27
- expect(() => render(_jsx(CallWithoutProvider, {}))).toThrow(/Must be inside AuthProvider/);
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);
28
213
  });
29
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
+ }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,6 @@
1
- export type { AuthContextValue, AuthProviderProps } from './authentication/AuthContext';
2
- export { useAuthContext, AuthProvider } from './authentication/AuthContext';
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';
package/dist/index.js CHANGED
@@ -1 +1,6 @@
1
- export { useAuthContext, AuthProvider } from './authentication/AuthContext';
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
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@caseparts-org/casecore",
3
3
  "private": false,
4
- "version": "0.0.3",
4
+ "version": "0.0.4",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",