@redacto.io/consent-sdk-react 3.4.0 → 3.6.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 +12 -0
- package/LICENSE +176 -0
- package/dist/index.d.mts +51 -4
- package/dist/index.d.ts +51 -4
- package/dist/index.js +1912 -345
- package/dist/index.mjs +1911 -345
- package/dist/privacy-center.d.mts +566 -0
- package/dist/privacy-center.d.ts +566 -0
- package/dist/privacy-center.js +17174 -0
- package/dist/privacy-center.mjs +17136 -0
- package/package.json +47 -3
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +201 -118
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +1194 -93
- package/src/RedactoNoticeConsent/api/index.ts +337 -246
- package/src/RedactoNoticeConsent/api/types.ts +140 -41
- package/src/RedactoNoticeConsent/components/DigiLockerCallback.tsx +602 -0
- package/src/RedactoNoticeConsent/components/index.ts +7 -0
- package/src/RedactoNoticeConsent/injectStyles.ts +11 -0
- package/src/RedactoNoticeConsent/language-codes.ts +61 -0
- package/src/RedactoNoticeConsent/services/storage.ts +122 -0
- package/src/RedactoNoticeConsent/types.ts +2 -0
- package/src/RedactoNoticeConsentInline/api/index.ts +26 -64
- package/src/RedactoNoticeConsentInline/api/types.ts +0 -25
- package/src/RedactoPrivacyCenter/PrivacyCenterActivityPage.tsx +19 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterConsentManagerPage.tsx +17 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterFormPage.tsx +79 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +27 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +142 -0
- package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +51 -0
- package/src/RedactoPrivacyCenter/api/actions.ts +444 -0
- package/src/RedactoPrivacyCenter/api/client.ts +413 -0
- package/src/RedactoPrivacyCenter/api/config.ts +33 -0
- package/src/RedactoPrivacyCenter/api/fetcher.ts +181 -0
- package/src/RedactoPrivacyCenter/api/upload.ts +93 -0
- package/src/RedactoPrivacyCenter/assets/character.svg +31 -0
- package/src/RedactoPrivacyCenter/assets/fileAttach.svg +9 -0
- package/src/RedactoPrivacyCenter/assets/greenTick.svg +4 -0
- package/src/RedactoPrivacyCenter/assets/mail.svg +3 -0
- package/src/RedactoPrivacyCenter/assets/poweredByRedacto.svg +9 -0
- package/src/RedactoPrivacyCenter/assets/sentImg.svg +9 -0
- package/src/RedactoPrivacyCenter/assets/uploadFileIcon.svg +9 -0
- package/src/RedactoPrivacyCenter/components/ActivitiesList.tsx +272 -0
- package/src/RedactoPrivacyCenter/components/CaseDetailsModal.tsx +468 -0
- package/src/RedactoPrivacyCenter/components/CaseHistory.tsx +433 -0
- package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +726 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +239 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerConstants.ts +7 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerTableSkeleton.tsx +155 -0
- package/src/RedactoPrivacyCenter/components/DataRequest/CorrectionDataSection.tsx +189 -0
- package/src/RedactoPrivacyCenter/components/DataRequest/GrievanceDataSection.tsx +118 -0
- package/src/RedactoPrivacyCenter/components/DataRequest/NominationDataSection.tsx +66 -0
- package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +305 -0
- package/src/RedactoPrivacyCenter/components/Form.tsx +1455 -0
- package/src/RedactoPrivacyCenter/components/GrievanceTable.tsx +176 -0
- package/src/RedactoPrivacyCenter/components/GrievanceTablePagination.tsx +330 -0
- package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +329 -0
- package/src/RedactoPrivacyCenter/components/Pagination.tsx +140 -0
- package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +302 -0
- package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +218 -0
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideBarPC.tsx +475 -0
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideMenuPC.tsx +412 -0
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/constants.tsx +42 -0
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/menuItemsPC.tsx +1 -0
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/types.ts +40 -0
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/utils.ts +38 -0
- package/src/RedactoPrivacyCenter/components/ThemeTransitionOverlay.tsx +85 -0
- package/src/RedactoPrivacyCenter/components/activitiesColumns.tsx +141 -0
- package/src/RedactoPrivacyCenter/components/caseHistoryColumns.tsx +179 -0
- package/src/RedactoPrivacyCenter/components/constant.ts +20 -0
- package/src/RedactoPrivacyCenter/components/types.ts +68 -0
- package/src/RedactoPrivacyCenter/components/utils.tsx +10 -0
- package/src/RedactoPrivacyCenter/config/routes.ts +9 -0
- package/src/RedactoPrivacyCenter/config/table.ts +25 -0
- package/src/RedactoPrivacyCenter/context/AuthContext.tsx +123 -0
- package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +44 -0
- package/src/RedactoPrivacyCenter/context/LocaleContext.tsx +11 -0
- package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +64 -0
- package/src/RedactoPrivacyCenter/context/OrganizationContext.tsx +30 -0
- package/src/RedactoPrivacyCenter/context/PageChromeContext.tsx +60 -0
- package/src/RedactoPrivacyCenter/context/theme.tsx +127 -0
- package/src/RedactoPrivacyCenter/lib/api.ts +468 -0
- package/src/RedactoPrivacyCenter/lib/consent-manager/utils.tsx +63 -0
- package/src/RedactoPrivacyCenter/lib/constants.ts +116 -0
- package/src/RedactoPrivacyCenter/lib/i18n.ts +81 -0
- package/src/RedactoPrivacyCenter/lib/locales.ts +105 -0
- package/src/RedactoPrivacyCenter/lib/queries.ts +30 -0
- package/src/RedactoPrivacyCenter/lib/types.ts +346 -0
- package/src/RedactoPrivacyCenter/lib/useCurrentLanguage.ts +37 -0
- package/src/RedactoPrivacyCenter/lib/utils.ts +625 -0
- package/src/RedactoPrivacyCenter/locales/as/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/bho/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/bn/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/brx/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/doi/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/en/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/gom/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/gu/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/hi/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/kn/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/ks/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/mai/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/ml/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/mr/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/ne/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/or/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/pa/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/sa/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/sat/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/sd/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/ta/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/te/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/ur/translation.json +188 -0
- package/src/RedactoPrivacyCenter/styles/base.css +117 -0
- package/src/RedactoPrivacyCenter/styles/injectStyles.ts +1000 -0
- package/src/RedactoPrivacyCenter/styles/pcStyles.ts +819 -0
- package/src/RedactoPrivacyCenter/styles/responsive.ts +138 -0
- package/src/RedactoPrivacyCenter/styles/variables.css +133 -0
- package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +365 -0
- package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +479 -0
- package/src/RedactoPrivacyCenter/table/index.ts +2 -0
- package/src/RedactoPrivacyCenter/types/images.d.ts +29 -0
- package/src/RedactoPrivacyCenter/ui/Footer.tsx +73 -0
- package/src/RedactoPrivacyCenter/ui/FormBlockWrapper.tsx +54 -0
- package/src/RedactoPrivacyCenter/ui/PCAccordion.tsx +87 -0
- package/src/RedactoPrivacyCenter/ui/PCActionIcon.tsx +47 -0
- package/src/RedactoPrivacyCenter/ui/PCAvatar.tsx +69 -0
- package/src/RedactoPrivacyCenter/ui/PCBadge.tsx +48 -0
- package/src/RedactoPrivacyCenter/ui/PCButton.tsx +77 -0
- package/src/RedactoPrivacyCenter/ui/PCCheckbox.tsx +55 -0
- package/src/RedactoPrivacyCenter/ui/PCCollapse.tsx +97 -0
- package/src/RedactoPrivacyCenter/ui/PCEmpty.tsx +35 -0
- package/src/RedactoPrivacyCenter/ui/PCInput.tsx +128 -0
- package/src/RedactoPrivacyCenter/ui/PCLoader.tsx +30 -0
- package/src/RedactoPrivacyCenter/ui/PCModal.tsx +104 -0
- package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +175 -0
- package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +381 -0
- package/src/RedactoPrivacyCenter/ui/PCTable.tsx +137 -0
- package/src/RedactoPrivacyCenter/ui/PCText.tsx +39 -0
- package/src/RedactoPrivacyCenter/ui/PCTextarea.tsx +43 -0
- package/src/RedactoPrivacyCenter/ui/PCTitle.tsx +37 -0
- package/src/RedactoPrivacyCenter/ui/PCTooltip.tsx +119 -0
- package/src/RedactoPrivacyCenter/ui/SimpleBar.tsx +25 -0
- package/src/RedactoPrivacyCenter/ui/StatusBadge.tsx +55 -0
- package/src/RedactoPrivacyCenter/ui/ThemeToggle.tsx +70 -0
- package/src/RedactoPrivacyCenter/ui/index.ts +18 -0
- package/src/RedactoPrivacyCenter/utils/cn.ts +28 -0
- package/src/RedactoPrivacyCenter/utils/date.ts +48 -0
- package/src/RedactoPrivacyCenter/utils/status.ts +111 -0
- package/src/RedactoPrivacyCenter/utils/toast.ts +130 -0
- package/src/index.ts +10 -0
- package/src/privacy-center.ts +107 -0
- package/tsconfig.json +2 -0
- package/tsup.config.ts +16 -1
- package/src/utils/isLegacyApi.ts +0 -12
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
import { refreshOtpTokenServer } from './api';
|
|
2
|
+
import type { BaseUrlContextValue } from '../context/BaseUrlContext';
|
|
3
|
+
|
|
4
|
+
// Token refresh lock to prevent concurrent refresh attempts
|
|
5
|
+
let isRefreshing = false;
|
|
6
|
+
let refreshPromise: Promise<string | null> | null = null;
|
|
7
|
+
|
|
8
|
+
// Buffer time in seconds before token expiration to proactively refresh
|
|
9
|
+
// Using 30 seconds to ensure tokens are refreshed before they expire
|
|
10
|
+
const TOKEN_EXPIRY_BUFFER_SECONDS = 30;
|
|
11
|
+
|
|
12
|
+
export const getCookieValue = (name: string) => {
|
|
13
|
+
const cookies = document.cookie.split('; ');
|
|
14
|
+
for (let cookie of cookies) {
|
|
15
|
+
const [key, value] = cookie.split('=');
|
|
16
|
+
if (key === name) {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Set refresh token cookie with proper attributes
|
|
25
|
+
* @param refreshToken - The refresh token value to set
|
|
26
|
+
*/
|
|
27
|
+
export const setRefreshTokenCookie = (refreshToken: string): void => {
|
|
28
|
+
if (typeof window === 'undefined') {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const cookieAttributes = [
|
|
33
|
+
`refresh_token=${refreshToken}`,
|
|
34
|
+
'path=/privacy-center',
|
|
35
|
+
`max-age=${60 * 60 * 24 * 7}`, // 7 days
|
|
36
|
+
`samesite=lax`,
|
|
37
|
+
]
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.join('; ');
|
|
40
|
+
|
|
41
|
+
document.cookie = cookieAttributes;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Decode JWT token and extract payload
|
|
46
|
+
* @param token - JWT token string
|
|
47
|
+
* @returns Decoded JWT payload or null if decoding fails
|
|
48
|
+
*/
|
|
49
|
+
export const decodeJwtToken = (token: string): { organisation_uuid?: string; organisation_name?: string; workspace_uuid?: string; [key: string]: any } | null => {
|
|
50
|
+
if (!token || typeof token !== 'string') {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
// JWT format: header.payload.signature
|
|
56
|
+
const parts = token.split('.');
|
|
57
|
+
if (parts.length !== 3) {
|
|
58
|
+
// Malformed JWT
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Decode payload (second part)
|
|
63
|
+
const payload = parts[1];
|
|
64
|
+
|
|
65
|
+
// Base64URL decode
|
|
66
|
+
// Replace URL-safe characters and add padding if needed
|
|
67
|
+
let base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
|
|
68
|
+
const padding = base64.length % 4;
|
|
69
|
+
if (padding) {
|
|
70
|
+
base64 += '='.repeat(4 - padding);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Decode base64
|
|
74
|
+
const decodedPayload = atob(base64);
|
|
75
|
+
const payloadObj = JSON.parse(decodedPayload) as { organisation_uuid?: string; organisation_name?: string; workspace_uuid?: string; [key: string]: any };
|
|
76
|
+
|
|
77
|
+
return payloadObj;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
// If decoding fails, return null
|
|
80
|
+
console.error('Error decoding JWT:', error);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Decode JWT payload and check if token is expired or will expire soon
|
|
87
|
+
* @param token - JWT token string
|
|
88
|
+
* @param bufferSeconds - Buffer time in seconds before expiration to consider token expired (default: 5 minutes)
|
|
89
|
+
* @returns true if token is expired, invalid, or will expire within buffer time; false if valid
|
|
90
|
+
*/
|
|
91
|
+
export const isJwtExpired = (token: string, bufferSeconds: number = 5 * 60): boolean => {
|
|
92
|
+
if (!token || typeof token !== 'string') {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// JWT format: header.payload.signature
|
|
98
|
+
const parts = token.split('.');
|
|
99
|
+
if (parts.length !== 3) {
|
|
100
|
+
// Malformed JWT
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Decode payload (second part)
|
|
105
|
+
const payload = parts[1];
|
|
106
|
+
|
|
107
|
+
// Base64URL decode
|
|
108
|
+
// Replace URL-safe characters and add padding if needed
|
|
109
|
+
let base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
|
|
110
|
+
const padding = base64.length % 4;
|
|
111
|
+
if (padding) {
|
|
112
|
+
base64 += '='.repeat(4 - padding);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Decode base64
|
|
116
|
+
const decodedPayload = atob(base64);
|
|
117
|
+
const payloadObj: { exp?: number } = JSON.parse(decodedPayload) as { exp?: number };
|
|
118
|
+
|
|
119
|
+
// Check if exp claim exists
|
|
120
|
+
if (typeof payloadObj.exp !== 'number') {
|
|
121
|
+
// Missing exp claim - treat as invalid
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Get current time in seconds
|
|
126
|
+
const now = Math.floor(Date.now() / 1000);
|
|
127
|
+
|
|
128
|
+
// Check if token is expired or will expire within buffer time
|
|
129
|
+
const isExpired = payloadObj.exp <= (now + bufferSeconds);
|
|
130
|
+
|
|
131
|
+
// exp is in seconds since epoch
|
|
132
|
+
return isExpired;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
// If decoding fails, treat as expired/invalid
|
|
135
|
+
console.error('Error decoding JWT:', error);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check if user is authenticated in privacy center
|
|
142
|
+
* User is considered authenticated if both access_token and email exist in sessionStorage
|
|
143
|
+
*/
|
|
144
|
+
export const isUserAuthenticated = (expectedSlug: string): boolean => {
|
|
145
|
+
if (typeof window === 'undefined') return false;
|
|
146
|
+
const accessToken = sessionStorage.getItem('access_token');
|
|
147
|
+
const email = sessionStorage.getItem('email');
|
|
148
|
+
const orgSlug = sessionStorage.getItem('pc_org_slug');
|
|
149
|
+
|
|
150
|
+
// Require that tokens exist and the stored organization slug matches
|
|
151
|
+
if (!accessToken || !email || !orgSlug) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return orgSlug === expectedSlug;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get initial state for sign-in form visibility
|
|
160
|
+
* Returns true if window is undefined (SSR) or user is not authenticated
|
|
161
|
+
* @param slug - The organization slug to check authentication against
|
|
162
|
+
* @returns true if sign-in form should be shown, false otherwise
|
|
163
|
+
*/
|
|
164
|
+
export const getInitialState = (slug: string): boolean => {
|
|
165
|
+
if (typeof window === 'undefined') return true;
|
|
166
|
+
return !isUserAuthenticated(slug);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Check authentication status and attempt token refresh if needed
|
|
171
|
+
* This function checks if the user is authenticated, validates JWT expiration,
|
|
172
|
+
* and attempts to refresh using the refresh token cookie if the token is expired or missing.
|
|
173
|
+
* @param expectedSlug - The organization slug to verify against
|
|
174
|
+
* @param organizationId - Optional organization ID (will be read from sessionStorage if not provided)
|
|
175
|
+
* @param workspaceId - Optional workspace ID (will be read from sessionStorage if not provided)
|
|
176
|
+
* @returns Promise<boolean> - true if authenticated with valid non-expired token, false otherwise
|
|
177
|
+
*/
|
|
178
|
+
export const checkAndRefreshAuth = async (
|
|
179
|
+
expectedSlug: string,
|
|
180
|
+
organizationId?: string,
|
|
181
|
+
workspaceId?: string,
|
|
182
|
+
consentClient?: BaseUrlContextValue['consentClient']
|
|
183
|
+
): Promise<boolean> => {
|
|
184
|
+
if (typeof window === 'undefined') return false;
|
|
185
|
+
|
|
186
|
+
const accessToken = sessionStorage.getItem('access_token');
|
|
187
|
+
const email = sessionStorage.getItem('email');
|
|
188
|
+
const orgSlug = sessionStorage.getItem('pc_org_slug');
|
|
189
|
+
|
|
190
|
+
// If slug doesn't match, user is not authenticated for this organization
|
|
191
|
+
if (orgSlug && orgSlug !== expectedSlug) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check if we need to refresh (token missing or expired)
|
|
196
|
+
const needsRefresh = !accessToken || (accessToken && isJwtExpired(accessToken, TOKEN_EXPIRY_BUFFER_SECONDS));
|
|
197
|
+
|
|
198
|
+
// If we have a valid token and all required data, user is authenticated
|
|
199
|
+
if (!needsRefresh && accessToken && email && orgSlug && orgSlug === expectedSlug) {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// If token is missing or expired, and we have email and orgSlug, try to refresh
|
|
204
|
+
if (needsRefresh && email && orgSlug === expectedSlug) {
|
|
205
|
+
// Get organizationId and workspaceId from sessionStorage if not provided
|
|
206
|
+
const orgId = organizationId || sessionStorage.getItem('pc_org_id') || '';
|
|
207
|
+
const wsId = workspaceId || sessionStorage.getItem('pc_workspace_id') || '';
|
|
208
|
+
|
|
209
|
+
if (!orgId || !wsId) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const refreshToken = getCookieValue('refresh_token');
|
|
215
|
+
if (!refreshToken) {
|
|
216
|
+
// No refresh token available - user needs to sign in again
|
|
217
|
+
signOutPrivacyCenter();
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const result = await refreshOtpTokenServer(orgId, wsId, refreshToken, consentClient);
|
|
222
|
+
if (result && result.success !== false) {
|
|
223
|
+
const newAccessToken = (result as { access_token?: string })?.access_token;
|
|
224
|
+
const newRefreshToken = (result as { refresh_token?: string })?.refresh_token;
|
|
225
|
+
|
|
226
|
+
if (newAccessToken) {
|
|
227
|
+
// Validate the new token is not expired before storing
|
|
228
|
+
if (isJwtExpired(newAccessToken, TOKEN_EXPIRY_BUFFER_SECONDS)) {
|
|
229
|
+
console.error('Refreshed access token is already expired - clearing auth state');
|
|
230
|
+
signOutPrivacyCenter();
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
sessionStorage.setItem('access_token', newAccessToken);
|
|
234
|
+
} else {
|
|
235
|
+
// No access token in response - refresh failed
|
|
236
|
+
console.error('Refresh response missing access token - clearing auth state');
|
|
237
|
+
signOutPrivacyCenter();
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (newRefreshToken) {
|
|
242
|
+
setRefreshTokenCookie(newRefreshToken);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Verify we now have a valid, non-expired access token
|
|
246
|
+
const isValid = !!newAccessToken && !isJwtExpired(newAccessToken, TOKEN_EXPIRY_BUFFER_SECONDS);
|
|
247
|
+
if (!isValid) {
|
|
248
|
+
console.error('Refreshed token validation failed - clearing auth state');
|
|
249
|
+
signOutPrivacyCenter();
|
|
250
|
+
}
|
|
251
|
+
return isValid;
|
|
252
|
+
} else {
|
|
253
|
+
// Refresh token expired/revoked or server returned failure
|
|
254
|
+
// This is an expected scenario - silently handle it
|
|
255
|
+
signOutPrivacyCenter();
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
} catch (error: any) {
|
|
259
|
+
// Check if error indicates refresh token is expired/revoked (expected scenario)
|
|
260
|
+
const errorMessage = error?.message || '';
|
|
261
|
+
const isRefreshTokenExpired =
|
|
262
|
+
errorMessage.includes('revoked') ||
|
|
263
|
+
errorMessage.includes('expired') ||
|
|
264
|
+
errorMessage.includes('Refresh token') ||
|
|
265
|
+
errorMessage.includes('Invalid refresh token') ||
|
|
266
|
+
errorMessage.includes('401') ||
|
|
267
|
+
errorMessage.includes('Unauthorized');
|
|
268
|
+
|
|
269
|
+
if (isRefreshTokenExpired) {
|
|
270
|
+
// Refresh token is expired/revoked - silently handle (expected scenario)
|
|
271
|
+
signOutPrivacyCenter();
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// For other errors (network, etc.), log and don't clear auth state - might be temporary
|
|
276
|
+
console.error('[Token Refresh] Unexpected error refreshing token:', error);
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return false;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Compare two email addresses (case-insensitive, handles URL encoding)
|
|
286
|
+
* @param email1 - First email (can be URL-encoded)
|
|
287
|
+
* @param email2 - Second email
|
|
288
|
+
* @returns true if emails match (case-insensitive), false otherwise
|
|
289
|
+
*/
|
|
290
|
+
export const compareEmails = (email1: string | null, email2: string | null): boolean => {
|
|
291
|
+
if (!email1 || !email2) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Decode URL-encoded email (handles %40 for @, %2B for +, etc.)
|
|
296
|
+
const decodedEmail1 = decodeURIComponent(email1.trim());
|
|
297
|
+
const normalizedEmail1 = decodedEmail1.toLowerCase();
|
|
298
|
+
const normalizedEmail2 = email2.trim().toLowerCase();
|
|
299
|
+
|
|
300
|
+
return normalizedEmail1 === normalizedEmail2;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Sign out from privacy center only
|
|
305
|
+
* Clears privacy-center specific storage and cookies without touching Supabase auth
|
|
306
|
+
*/
|
|
307
|
+
export const signOutPrivacyCenter = (): void => {
|
|
308
|
+
if (typeof window === 'undefined') return;
|
|
309
|
+
|
|
310
|
+
const privacyCenterKeys = [
|
|
311
|
+
'access_token',
|
|
312
|
+
'email',
|
|
313
|
+
'i18nextLng',
|
|
314
|
+
'lang',
|
|
315
|
+
'pc_org_slug',
|
|
316
|
+
'pc_org_id',
|
|
317
|
+
'pc_workspace_id',
|
|
318
|
+
'pc_current_page',
|
|
319
|
+
'pc_grievance_view',
|
|
320
|
+
];
|
|
321
|
+
privacyCenterKeys.forEach((key) => {
|
|
322
|
+
sessionStorage.removeItem(key);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Clear refresh token cookie set for privacy center
|
|
326
|
+
document.cookie =
|
|
327
|
+
'refresh_token=; path=/privacy-center; expires=Thu, 01 Jan 1970 00:00:00 GMT';
|
|
328
|
+
// Fallback in case the cookie path differs
|
|
329
|
+
document.cookie =
|
|
330
|
+
'refresh_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Ensure a valid access token is available before making API calls.
|
|
335
|
+
* This function checks if the current token is valid (not expired).
|
|
336
|
+
* If expired or missing, it attempts to refresh using the refresh token.
|
|
337
|
+
* If refresh fails (refresh token expired), it signs out and redirects to sign-in.
|
|
338
|
+
*
|
|
339
|
+
* @param organizationId - The organization UUID
|
|
340
|
+
* @param workspaceId - The workspace UUID
|
|
341
|
+
* @param slug - The organization slug for redirect
|
|
342
|
+
* @param authContext - Optional auth context values (accessToken, refreshToken, onError, updateTokens)
|
|
343
|
+
* @returns Promise<string | null> - Valid access token or null if auth failed
|
|
344
|
+
*/
|
|
345
|
+
export const ensureValidToken = async (
|
|
346
|
+
organizationId?: string,
|
|
347
|
+
workspaceId?: string,
|
|
348
|
+
slug?: string,
|
|
349
|
+
authContext?: {
|
|
350
|
+
accessToken?: string;
|
|
351
|
+
refreshToken?: string;
|
|
352
|
+
onError?: (error: Error) => boolean;
|
|
353
|
+
updateTokens?: (accessToken: string, refreshToken?: string) => void;
|
|
354
|
+
},
|
|
355
|
+
consentClient?: BaseUrlContextValue['consentClient']
|
|
356
|
+
): Promise<string | null> => {
|
|
357
|
+
if (typeof window === 'undefined') return null;
|
|
358
|
+
|
|
359
|
+
// Use context accessToken if provided, otherwise fall back to sessionStorage
|
|
360
|
+
const accessToken = authContext?.accessToken || sessionStorage.getItem('access_token');
|
|
361
|
+
|
|
362
|
+
// Check if current token is valid (exists, is a string, and not expired)
|
|
363
|
+
// Use a buffer to proactively refresh before actual expiry
|
|
364
|
+
if (
|
|
365
|
+
accessToken &&
|
|
366
|
+
typeof accessToken === 'string' &&
|
|
367
|
+
accessToken.trim() !== '' &&
|
|
368
|
+
!isJwtExpired(accessToken, TOKEN_EXPIRY_BUFFER_SECONDS)
|
|
369
|
+
) {
|
|
370
|
+
return accessToken;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Token is missing or expired, need to refresh
|
|
374
|
+
// Use lock to prevent concurrent refresh attempts
|
|
375
|
+
if (isRefreshing && refreshPromise) {
|
|
376
|
+
// Wait for ongoing refresh to complete
|
|
377
|
+
const newToken = await refreshPromise;
|
|
378
|
+
// Return the new token directly (it's already stored in sessionStorage/context)
|
|
379
|
+
return newToken;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Start refresh process
|
|
383
|
+
isRefreshing = true;
|
|
384
|
+
refreshPromise = performTokenRefresh(organizationId, workspaceId, slug, authContext, consentClient);
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
const newToken = await refreshPromise;
|
|
388
|
+
// Return the new token directly (it's already stored in sessionStorage/context)
|
|
389
|
+
return newToken;
|
|
390
|
+
} finally {
|
|
391
|
+
isRefreshing = false;
|
|
392
|
+
refreshPromise = null;
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Internal function to perform token refresh
|
|
398
|
+
* @returns Promise<string | null> - The new access token on success, null on failure
|
|
399
|
+
*/
|
|
400
|
+
const performTokenRefresh = async (
|
|
401
|
+
organizationId?: string,
|
|
402
|
+
workspaceId?: string,
|
|
403
|
+
slug?: string,
|
|
404
|
+
authContext?: {
|
|
405
|
+
accessToken?: string;
|
|
406
|
+
refreshToken?: string;
|
|
407
|
+
onError?: (error: Error) => boolean;
|
|
408
|
+
updateTokens?: (accessToken: string, refreshToken?: string) => void;
|
|
409
|
+
},
|
|
410
|
+
consentClient?: BaseUrlContextValue['consentClient']
|
|
411
|
+
): Promise<string | null> => {
|
|
412
|
+
// Get organizationId and workspaceId from sessionStorage if not provided
|
|
413
|
+
const orgId = organizationId || sessionStorage.getItem('pc_org_id') || '';
|
|
414
|
+
const wsId = workspaceId || sessionStorage.getItem('pc_workspace_id') || '';
|
|
415
|
+
const orgSlug = slug || sessionStorage.getItem('pc_org_slug') || '';
|
|
416
|
+
|
|
417
|
+
if (!orgId || !wsId) {
|
|
418
|
+
console.error('[Token Refresh] Missing organizationId or workspaceId');
|
|
419
|
+
const error = new Error('Missing organizationId or workspaceId');
|
|
420
|
+
// Call onError if provided, check return value before proceeding
|
|
421
|
+
if (authContext?.onError) {
|
|
422
|
+
const shouldProceed = authContext.onError(error);
|
|
423
|
+
if (!shouldProceed) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
signOutPrivacyCenter();
|
|
428
|
+
redirectToSignIn(orgSlug, authContext?.onError);
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Use context refreshToken if provided, otherwise fall back to cookie
|
|
433
|
+
// If authContext is provided, prioritize its refreshToken (props mode)
|
|
434
|
+
// Otherwise, fall back to cookie (legacy mode)
|
|
435
|
+
let refreshTokenValue: string | null = null;
|
|
436
|
+
|
|
437
|
+
if (authContext) {
|
|
438
|
+
// Props mode: use refreshToken from context
|
|
439
|
+
refreshTokenValue = authContext.refreshToken || null;
|
|
440
|
+
} else {
|
|
441
|
+
// Legacy mode: use refreshToken from cookie
|
|
442
|
+
refreshTokenValue = getCookieValue('refresh_token');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Validate refreshToken is a non-empty string
|
|
446
|
+
if (!refreshTokenValue || typeof refreshTokenValue !== 'string' || refreshTokenValue.trim() === '') {
|
|
447
|
+
// No refresh token available - user needs to sign in again
|
|
448
|
+
console.warn('[Token Refresh] No valid refresh token available', {
|
|
449
|
+
hasAuthContext: !!authContext,
|
|
450
|
+
refreshTokenFromContext: authContext?.refreshToken,
|
|
451
|
+
refreshTokenFromCookie: getCookieValue('refresh_token'),
|
|
452
|
+
});
|
|
453
|
+
const error = new Error('No refresh token available');
|
|
454
|
+
// Call onError if provided, check return value before proceeding
|
|
455
|
+
if (authContext?.onError) {
|
|
456
|
+
const shouldProceed = authContext.onError(error);
|
|
457
|
+
if (!shouldProceed) {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
signOutPrivacyCenter();
|
|
462
|
+
redirectToSignIn(orgSlug, authContext?.onError);
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// At this point, refreshTokenValue is guaranteed to be a non-empty string
|
|
467
|
+
const refreshToken: string = refreshTokenValue;
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
const result = await refreshOtpTokenServer(orgId, wsId, refreshToken, consentClient);
|
|
471
|
+
|
|
472
|
+
if (result && result.success !== false) {
|
|
473
|
+
const newAccessToken = (result as { access_token?: string })?.access_token;
|
|
474
|
+
const newRefreshToken = (result as { refresh_token?: string })?.refresh_token;
|
|
475
|
+
|
|
476
|
+
if (newAccessToken) {
|
|
477
|
+
// Validate the new token is not expired before storing
|
|
478
|
+
if (isJwtExpired(newAccessToken, TOKEN_EXPIRY_BUFFER_SECONDS)) {
|
|
479
|
+
console.error('[Token Refresh] New access token is already expired');
|
|
480
|
+
const error = new Error('New access token is already expired');
|
|
481
|
+
// Call onError if provided, check return value before proceeding
|
|
482
|
+
if (authContext?.onError) {
|
|
483
|
+
const shouldProceed = authContext.onError(error);
|
|
484
|
+
if (!shouldProceed) {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
signOutPrivacyCenter();
|
|
489
|
+
redirectToSignIn(orgSlug, authContext?.onError);
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Update tokens via context if available, otherwise use storage
|
|
494
|
+
if (authContext?.updateTokens) {
|
|
495
|
+
authContext.updateTokens(newAccessToken, newRefreshToken);
|
|
496
|
+
} else {
|
|
497
|
+
sessionStorage.setItem('access_token', newAccessToken);
|
|
498
|
+
if (newRefreshToken) {
|
|
499
|
+
setRefreshTokenCookie(newRefreshToken);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Return the new access token directly (already stored in sessionStorage/context)
|
|
504
|
+
return newAccessToken;
|
|
505
|
+
} else {
|
|
506
|
+
// No access token in response - refresh failed
|
|
507
|
+
console.error('[Token Refresh] Response missing access token');
|
|
508
|
+
const error = new Error('Response missing access token');
|
|
509
|
+
// Call onError if provided, check return value before proceeding
|
|
510
|
+
if (authContext?.onError) {
|
|
511
|
+
const shouldProceed = authContext.onError(error);
|
|
512
|
+
if (!shouldProceed) {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
signOutPrivacyCenter();
|
|
517
|
+
redirectToSignIn(orgSlug, authContext?.onError);
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
} else {
|
|
521
|
+
// Refresh token expired/revoked or server returned failure
|
|
522
|
+
const error = new Error('Refresh token expired or revoked');
|
|
523
|
+
// Call onError if provided, check return value before proceeding
|
|
524
|
+
if (authContext?.onError) {
|
|
525
|
+
const shouldProceed = authContext.onError(error);
|
|
526
|
+
if (!shouldProceed) {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
signOutPrivacyCenter();
|
|
531
|
+
redirectToSignIn(orgSlug, authContext?.onError);
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
} catch (error: any) {
|
|
535
|
+
const errorMessage = error?.message || '';
|
|
536
|
+
const isRefreshTokenExpired =
|
|
537
|
+
errorMessage.includes('revoked') ||
|
|
538
|
+
errorMessage.includes('expired') ||
|
|
539
|
+
errorMessage.includes('Refresh token') ||
|
|
540
|
+
errorMessage.includes('Invalid refresh token') ||
|
|
541
|
+
errorMessage.includes('401') ||
|
|
542
|
+
errorMessage.includes('Unauthorized');
|
|
543
|
+
|
|
544
|
+
if (isRefreshTokenExpired) {
|
|
545
|
+
// Refresh token is expired/revoked - expected scenario
|
|
546
|
+
const err = new Error('Refresh token expired or revoked');
|
|
547
|
+
// Call onError if provided, check return value before proceeding
|
|
548
|
+
if (authContext?.onError) {
|
|
549
|
+
const shouldProceed = authContext.onError(err);
|
|
550
|
+
if (!shouldProceed) {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
signOutPrivacyCenter();
|
|
555
|
+
redirectToSignIn(orgSlug, authContext?.onError);
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// For other errors (network, etc.), log but don't clear auth state
|
|
560
|
+
console.error('[Token Refresh] Unexpected error:', error);
|
|
561
|
+
// Still call onError for unexpected errors
|
|
562
|
+
if (authContext?.onError) {
|
|
563
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
564
|
+
authContext.onError(err);
|
|
565
|
+
}
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Helper to redirect to sign-in page
|
|
572
|
+
*/
|
|
573
|
+
const redirectToSignIn = (slug?: string, onError?: (error: Error) => boolean): void => {
|
|
574
|
+
if (typeof window === 'undefined') return;
|
|
575
|
+
|
|
576
|
+
// If onError is provided, call it and check return value
|
|
577
|
+
if (onError) {
|
|
578
|
+
const error = new Error('Authentication failed - redirecting to sign-in');
|
|
579
|
+
const shouldProceed = onError(error);
|
|
580
|
+
if (!shouldProceed) {
|
|
581
|
+
// User wants to handle navigation themselves
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const orgSlug = slug || sessionStorage.getItem('pc_org_slug') || '';
|
|
587
|
+
if (orgSlug) {
|
|
588
|
+
window.location.href = `/privacy-center/${orgSlug}/sign-in`;
|
|
589
|
+
} else {
|
|
590
|
+
window.location.href = '/privacy-center';
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
// Activity helper functions
|
|
595
|
+
|
|
596
|
+
export type ActivityBadgeStatus =
|
|
597
|
+
| 'success'
|
|
598
|
+
| 'warning'
|
|
599
|
+
| 'error'
|
|
600
|
+
| 'info'
|
|
601
|
+
| 'pending';
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Get badge status based on activity type
|
|
605
|
+
*/
|
|
606
|
+
export const getActivityTypeStatus = (
|
|
607
|
+
activityType: string
|
|
608
|
+
): ActivityBadgeStatus => {
|
|
609
|
+
const type = activityType.toLowerCase();
|
|
610
|
+
if (type.includes('consent') || type.includes('grant')) return 'success';
|
|
611
|
+
if (type.includes('revoke') || type.includes('withdraw')) return 'error';
|
|
612
|
+
if (type.includes('update') || type.includes('modify')) return 'warning';
|
|
613
|
+
if (type.includes('view') || type.includes('access')) return 'info';
|
|
614
|
+
return 'pending';
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Format activity type for display
|
|
619
|
+
*/
|
|
620
|
+
export const formatActivityType = (activityType: string): string => {
|
|
621
|
+
return activityType
|
|
622
|
+
.split('_')
|
|
623
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
624
|
+
.join(' ');
|
|
625
|
+
};
|