@crownpeak/dqm-react-component 1.0.1 → 1.2.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.
Files changed (178) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +125 -23
  3. package/dist/DQMSidebar.d.ts.map +1 -1
  4. package/dist/ErrorBoundary.d.ts.map +1 -1
  5. package/dist/__tests__/setup.d.ts +1 -0
  6. package/dist/__tests__/setup.d.ts.map +1 -0
  7. package/dist/__tests__/utils.d.ts +145 -0
  8. package/dist/__tests__/utils.d.ts.map +1 -0
  9. package/dist/auth-ui/assets/index-YKFZYENy.js +158 -0
  10. package/dist/auth-ui/index.html +1 -1
  11. package/dist/components/auth/DQMLogin.d.ts.map +1 -1
  12. package/dist/components/auth/index.d.ts +0 -1
  13. package/dist/components/auth/index.d.ts.map +1 -1
  14. package/dist/components/cards/AISummaryCard.d.ts +2 -0
  15. package/dist/components/cards/AISummaryCard.d.ts.map +1 -0
  16. package/dist/components/cards/index.d.ts +1 -0
  17. package/dist/components/cards/index.d.ts.map +1 -1
  18. package/dist/components/common/LanguageSwitch.d.ts +5 -0
  19. package/dist/components/common/LanguageSwitch.d.ts.map +1 -0
  20. package/dist/components/common/LanguageSwitchBase.d.ts +16 -0
  21. package/dist/components/common/LanguageSwitchBase.d.ts.map +1 -0
  22. package/dist/components/common/index.d.ts +1 -0
  23. package/dist/components/common/index.d.ts.map +1 -1
  24. package/dist/components/modals/AISettingsDialog.d.ts +61 -0
  25. package/dist/components/modals/AISettingsDialog.d.ts.map +1 -0
  26. package/dist/components/modals/HighlightModal.d.ts +45 -0
  27. package/dist/components/modals/HighlightModal.d.ts.map +1 -0
  28. package/dist/components/modals/LoginOverlay.d.ts +22 -0
  29. package/dist/components/modals/LoginOverlay.d.ts.map +1 -0
  30. package/dist/components/modals/index.d.ts +9 -0
  31. package/dist/components/modals/index.d.ts.map +1 -0
  32. package/dist/components/renderers/BrowserViewRenderer.d.ts.map +1 -1
  33. package/dist/components/renderers/ShadowDOMRenderer.d.ts.map +1 -1
  34. package/dist/components/sidebar/CloseButton.d.ts +6 -0
  35. package/dist/components/sidebar/CloseButton.d.ts.map +1 -0
  36. package/dist/components/sidebar/SidebarContent.d.ts.map +1 -1
  37. package/dist/components/sidebar/SidebarFooter.d.ts.map +1 -1
  38. package/dist/components/sidebar/SidebarHeader.d.ts.map +1 -1
  39. package/dist/components/sidebar/SidebarSkeleton.d.ts.map +1 -1
  40. package/dist/components/sidebar/StyledDrawer.d.ts +6 -1
  41. package/dist/components/sidebar/StyledDrawer.d.ts.map +1 -1
  42. package/dist/components/sidebar/StyledFab.d.ts +4 -1
  43. package/dist/components/sidebar/StyledFab.d.ts.map +1 -1
  44. package/dist/components/sidebar/index.d.ts +1 -0
  45. package/dist/components/sidebar/index.d.ts.map +1 -1
  46. package/dist/context/ai/AIContext.d.ts +13 -0
  47. package/dist/context/ai/AIContext.d.ts.map +1 -0
  48. package/dist/context/ai/index.d.ts +12 -0
  49. package/dist/context/ai/index.d.ts.map +1 -0
  50. package/dist/context/ai/types.d.ts +161 -0
  51. package/dist/context/ai/types.d.ts.map +1 -0
  52. package/dist/context/ai/useAIEngine.d.ts +10 -0
  53. package/dist/context/ai/useAIEngine.d.ts.map +1 -0
  54. package/dist/context/ai/useAISummary.d.ts +10 -0
  55. package/dist/context/ai/useAISummary.d.ts.map +1 -0
  56. package/dist/context/ai/useAITranslation.d.ts +10 -0
  57. package/dist/context/ai/useAITranslation.d.ts.map +1 -0
  58. package/dist/context/ai/useTranslationCache.d.ts +9 -0
  59. package/dist/context/ai/useTranslationCache.d.ts.map +1 -0
  60. package/dist/dqm-widget.d.ts +107 -0
  61. package/dist/dqm-widget.esm.js +516 -0
  62. package/dist/dqm-widget.iife.js +183 -0
  63. package/dist/hooks/index.d.ts +10 -0
  64. package/dist/hooks/index.d.ts.map +1 -0
  65. package/dist/hooks/useAnalysis.d.ts +43 -0
  66. package/dist/hooks/useAnalysis.d.ts.map +1 -0
  67. package/dist/hooks/useAuthentication.d.ts +49 -0
  68. package/dist/hooks/useAuthentication.d.ts.map +1 -0
  69. package/dist/hooks/useHighlightActions.d.ts +37 -0
  70. package/dist/hooks/useHighlightActions.d.ts.map +1 -0
  71. package/dist/hooks/useHighlights.d.ts +72 -0
  72. package/dist/hooks/useHighlights.d.ts.map +1 -0
  73. package/dist/html-pages/DQMWidget.d.ts +36 -0
  74. package/dist/html-pages/DQMWidget.d.ts.map +1 -0
  75. package/dist/html-pages/index.d.ts +32 -0
  76. package/dist/html-pages/index.d.ts.map +1 -0
  77. package/dist/i18n/auth/de.d.ts +25 -0
  78. package/dist/i18n/auth/de.d.ts.map +1 -0
  79. package/dist/i18n/auth/en.d.ts +26 -0
  80. package/dist/i18n/auth/en.d.ts.map +1 -0
  81. package/dist/i18n/auth/es.d.ts +25 -0
  82. package/dist/i18n/auth/es.d.ts.map +1 -0
  83. package/dist/i18n/auth/index.d.ts +8 -0
  84. package/dist/i18n/auth/index.d.ts.map +1 -0
  85. package/dist/i18n/common/de.d.ts +18 -0
  86. package/dist/i18n/common/de.d.ts.map +1 -0
  87. package/dist/i18n/common/en.d.ts +19 -0
  88. package/dist/i18n/common/en.d.ts.map +1 -0
  89. package/dist/i18n/common/es.d.ts +18 -0
  90. package/dist/i18n/common/es.d.ts.map +1 -0
  91. package/dist/i18n/common/index.d.ts +8 -0
  92. package/dist/i18n/common/index.d.ts.map +1 -0
  93. package/dist/i18n/demo/de.d.ts +104 -0
  94. package/dist/i18n/demo/de.d.ts.map +1 -0
  95. package/dist/i18n/demo/en.d.ts +105 -0
  96. package/dist/i18n/demo/en.d.ts.map +1 -0
  97. package/dist/i18n/demo/es.d.ts +104 -0
  98. package/dist/i18n/demo/es.d.ts.map +1 -0
  99. package/dist/i18n/demo/index.d.ts +8 -0
  100. package/dist/i18n/demo/index.d.ts.map +1 -0
  101. package/dist/i18n/index.d.ts +673 -0
  102. package/dist/i18n/index.d.ts.map +1 -0
  103. package/dist/i18n/sidebar/de.d.ts +89 -0
  104. package/dist/i18n/sidebar/de.d.ts.map +1 -0
  105. package/dist/i18n/sidebar/en.d.ts +90 -0
  106. package/dist/i18n/sidebar/en.d.ts.map +1 -0
  107. package/dist/i18n/sidebar/es.d.ts +89 -0
  108. package/dist/i18n/sidebar/es.d.ts.map +1 -0
  109. package/dist/i18n/sidebar/index.d.ts +8 -0
  110. package/dist/i18n/sidebar/index.d.ts.map +1 -0
  111. package/dist/i18n.d.ts +8 -0
  112. package/dist/i18n.d.ts.map +1 -0
  113. package/dist/index.cjs +61 -31
  114. package/dist/index.cjs.map +1 -1
  115. package/dist/index.d.ts +9 -1
  116. package/dist/index.d.ts.map +1 -1
  117. package/dist/index.html +498 -0
  118. package/dist/index.js +18006 -7241
  119. package/dist/index.js.map +1 -1
  120. package/dist/locale.d.ts +25 -0
  121. package/dist/locale.d.ts.map +1 -0
  122. package/dist/mocks/browser.d.ts +23 -0
  123. package/dist/mocks/browser.d.ts.map +1 -0
  124. package/dist/mocks/handlers.d.ts +32 -0
  125. package/dist/mocks/handlers.d.ts.map +1 -0
  126. package/dist/mocks/index.d.ts +7 -0
  127. package/dist/mocks/index.d.ts.map +1 -0
  128. package/dist/mocks/server.d.ts +24 -0
  129. package/dist/mocks/server.d.ts.map +1 -0
  130. package/dist/server/routes/auth.js +0 -10
  131. package/dist/server/routes/auth.js.map +1 -1
  132. package/dist/store/api/dqmApi.d.ts +1793 -0
  133. package/dist/store/api/dqmApi.d.ts.map +1 -0
  134. package/dist/store/api/index.d.ts +6 -0
  135. package/dist/store/api/index.d.ts.map +1 -0
  136. package/dist/store/index.d.ts +57 -0
  137. package/dist/store/index.d.ts.map +1 -0
  138. package/dist/store/localeSlice.d.ts +6 -0
  139. package/dist/store/localeSlice.d.ts.map +1 -0
  140. package/dist/store/slices/aiSlice.d.ts +134 -0
  141. package/dist/store/slices/aiSlice.d.ts.map +1 -0
  142. package/dist/store/slices/analysisSlice.d.ts +54 -0
  143. package/dist/store/slices/analysisSlice.d.ts.map +1 -0
  144. package/dist/store/slices/authSlice.d.ts +170 -0
  145. package/dist/store/slices/authSlice.d.ts.map +1 -0
  146. package/dist/store/slices/highlightSlice.d.ts +188 -0
  147. package/dist/store/slices/highlightSlice.d.ts.map +1 -0
  148. package/dist/store/slices/index.d.ts +12 -0
  149. package/dist/store/slices/index.d.ts.map +1 -0
  150. package/dist/types.d.ts +109 -7
  151. package/dist/types.d.ts.map +1 -1
  152. package/dist/utils/aiJsonClient.d.ts +23 -0
  153. package/dist/utils/aiJsonClient.d.ts.map +1 -0
  154. package/dist/utils/colors/GenerateCategoryColors.d.ts.map +1 -1
  155. package/dist/utils/logger.d.ts +107 -0
  156. package/dist/utils/logger.d.ts.map +1 -0
  157. package/dist/utils/openaiJsonClient.d.ts +8 -0
  158. package/dist/utils/openaiJsonClient.d.ts.map +1 -0
  159. package/dist/utils/sanitizeHtmlDocument.d.ts +4 -0
  160. package/dist/utils/sanitizeHtmlDocument.d.ts.map +1 -0
  161. package/dist/utils/secureStorage.d.ts +95 -0
  162. package/dist/utils/secureStorage.d.ts.map +1 -0
  163. package/dist/utils/storage.d.ts.map +1 -1
  164. package/dist/utils/translationCache.d.ts +45 -0
  165. package/dist/utils/translationCache.d.ts.map +1 -0
  166. package/dist/utils/translationUtils.d.ts +52 -0
  167. package/dist/utils/translationUtils.d.ts.map +1 -0
  168. package/dist/utils/useDomPresence.d.ts +210 -0
  169. package/dist/utils/useDomPresence.d.ts.map +1 -0
  170. package/package.json +59 -12
  171. package/AUTHENTICATION.md +0 -281
  172. package/BACKEND-API.md +0 -1829
  173. package/DEVELOPMENT.md +0 -339
  174. package/EXAMPLES.md +0 -194
  175. package/QUICKSTART.md +0 -200
  176. package/dist/auth-ui/assets/index-CczTRrba.js +0 -158
  177. package/dist/components/auth/OAuth2CallbackHandler.d.ts +0 -15
  178. package/dist/components/auth/OAuth2CallbackHandler.d.ts.map +0 -1
package/BACKEND-API.md DELETED
@@ -1,1829 +0,0 @@
1
- # DQM Backend API - Architecture & Implementation Guide
2
-
3
- ## Overview
4
-
5
- The DQM Backend API provides a **secure proxy layer** between your React application and the Crownpeak DQM API. It enables **centralized credential management** through server-side storage, eliminating the need to expose DQM API keys in the browser.
6
-
7
- ## Two Operating Modes
8
-
9
- ### Mode 1: Direct API Key (Development/Testing)
10
- User provides Crownpeak credentials directly in the widget:
11
- ```
12
- User → Widget (API Key + Website ID) → Crownpeak DQM API
13
- ```
14
- **Use Case:** Quick testing, development environments, demos
15
-
16
- ### Mode 2: Backend Session with SSO (Production - RECOMMENDED)
17
- User authenticates with YOUR system, backend manages DQM credentials:
18
- ```
19
- User → Your OAuth/SSO → Your Backend → Retrieves DQM Credentials → Session Token → Widget → Backend Proxy → Crownpeak DQM API
20
- ```
21
- **Use Case:** Production deployments, enterprise SSO integration, centralized credential management
22
-
23
- ---
24
-
25
- ## Mode 2 Architecture (SSO/Backend - RECOMMENDED)
26
-
27
- ### Complete Authentication Flow
28
-
29
- ```mermaid
30
- sequenceDiagram
31
- autonumber
32
- actor User as 👤 User (Browser)
33
- participant Widget as DQM Widget<br/>(React Component)
34
- participant Popup as Login Popup Window
35
- participant Backend as Your Backend<br/>(Express/Node.js)
36
- participant OAuth as OAuth Provider<br/>(Okta/Azure AD/Auth0)
37
- participant DB as Your Database<br/>(User Credentials)
38
- participant Redis as Redis SessionStore
39
- participant DQM as Crownpeak DQM API<br/>(api.crownpeak.net)
40
-
41
- Note over User,DQM: Phase 1: Initial Authentication Request
42
- User->>Widget: Opens widget, clicks "Login"
43
- activate Widget
44
- Widget->>Popup: window.open('/auth/login')
45
- activate Popup
46
- Note over Popup: Beautiful Material-UI<br/>Login Page<br/>(Built from server-ui/)
47
- Popup-->>User: Display: "Sign in with SSO"<br/>or "Direct API Key"
48
- deactivate Widget
49
-
50
- Note over User,DQM: Phase 2: OAuth Authorization Flow
51
- User->>Popup: Clicks "Sign in with SSO"
52
- Popup->>OAuth: Redirect to OAuth authorize URL<br/>?client_id=X&redirect_uri=/auth/callback
53
- activate OAuth
54
- OAuth-->>User: Display login form
55
- User->>OAuth: Enter credentials
56
- OAuth->>OAuth: Validate user credentials
57
- OAuth->>Popup: Redirect: /auth/callback<br/>?code=xyz&state=abc
58
- deactivate OAuth
59
- activate Popup
60
- Note over Popup: Callback Handler Page<br/>Shows loading spinner
61
-
62
- Note over User,DQM: Phase 3: Token Exchange & Session Creation
63
- Popup->>Backend: POST /auth/oauth2/callback<br/>{ code, redirectUri }
64
- activate Backend
65
- Backend->>OAuth: POST /token<br/>{ grant_type, code,<br/>client_id, client_secret }
66
- activate OAuth
67
- OAuth-->>Backend: { access_token, ... }
68
- deactivate OAuth
69
-
70
- Backend->>OAuth: GET /userinfo<br/>Authorization: Bearer {token}
71
- activate OAuth
72
- OAuth-->>Backend: { sub: userId, email, ... }
73
- deactivate OAuth
74
-
75
- Backend->>DB: SELECT api_key, website_id<br/>WHERE user_id = $userId
76
- activate DB
77
- Note over DB: Table: user_dqm_credentials<br/>┌──────────┬─────────────┐<br/>│ user_id │ dqm_api_key │ (ENCRYPTED!)<br/>│ │ website_id │<br/>└──────────┴─────────────┘
78
- DB-->>Backend: { encrypted_api_key,<br/>website_id }
79
- deactivate DB
80
-
81
- Backend->>Backend: apiKey = decrypt(encrypted_api_key)
82
-
83
- Backend->>Redis: Create session<br/>sessionStore.create(apiKey,<br/>websiteId, userId)
84
- activate Redis
85
- Note over Redis: Store session data:<br/>session:{token}<br/>├─ apiKey: "real_key..."<br/>├─ websiteId: "site123"<br/>├─ userId: "user456"<br/>└─ expiresAt: timestamp<br/><br/>TTL: 24 hours
86
- Redis-->>Backend: sessionToken
87
- deactivate Redis
88
-
89
- Backend-->>Popup: 200 OK<br/>{ sessionToken,<br/>websiteId, userId }
90
- deactivate Backend
91
-
92
- Note over User,DQM: Phase 4: Token Propagation to Widget
93
- Popup->>Widget: postMessage({<br/>type: 'DQM_AUTH_SUCCESS',<br/>sessionToken, websiteId<br/>})
94
- activate Widget
95
- Popup->>Popup: window.close()
96
- deactivate Popup
97
-
98
- Widget->>Widget: Store in localStorage:<br/>- dqm_sessionToken<br/>- dqm_websiteID<br/>- dqm_sessionType: 'backend'
99
- Widget-->>User: Show: "Authenticated ✓"
100
- deactivate Widget
101
-
102
- Note over User,DQM: Phase 5: Proxied DQM API Requests
103
- User->>Widget: Analyze page quality
104
- activate Widget
105
- Widget->>Widget: Capture HTML via<br/>optimizeHtmlForAnalysis()
106
- Widget->>Backend: POST /dqm/assets<br/>Authorization: Bearer {sessionToken}<br/>{ html, url }
107
- activate Backend
108
-
109
- Backend->>Backend: Extract token from header
110
- Backend->>Redis: sessionStore.get(token)
111
- activate Redis
112
- Redis-->>Backend: { apiKey, websiteId, ... }
113
- deactivate Redis
114
-
115
- Backend->>Backend: Validate session.expiresAt
116
- Backend->>DQM: POST /dqm-cms/v1/assets<br/>x-api-key: {real_api_key}<br/>{ html, url, websiteId }
117
- activate DQM
118
- DQM-->>Backend: { assetId, analysisState }
119
- deactivate DQM
120
-
121
- Backend-->>Widget: { assetId, analysisState }
122
- deactivate Backend
123
-
124
- Note over Widget: Poll every 2s for completion
125
- loop Until analysisState === 'completed'
126
- Widget->>Backend: GET /dqm/assets/{assetId}<br/>Authorization: Bearer {sessionToken}
127
- activate Backend
128
- Backend->>Redis: Verify session
129
- activate Redis
130
- Redis-->>Backend: { apiKey }
131
- deactivate Redis
132
- Backend->>DQM: GET /assets/{assetId}?apiKey=X<br/>x-api-key: {real_api_key}
133
- activate DQM
134
- DQM-->>Backend: { checkpoints[], totalErrors }
135
- deactivate DQM
136
- Backend-->>Widget: Analysis data
137
- deactivate Backend
138
- end
139
-
140
- Widget-->>User: Display quality results<br/>(CategoryCards, Checkpoints)
141
- deactivate Widget
142
-
143
- Note over User,DQM: Phase 6: Error Highlighting (Optional)
144
- User->>Widget: Click specific checkpoint
145
- activate Widget
146
- Widget->>Backend: GET /dqm/assets/{assetId}/<br/>pagehighlight/{checkpointId}<br/>Authorization: Bearer {sessionToken}
147
- activate Backend
148
- Backend->>Redis: Verify session
149
- activate Redis
150
- Redis-->>Backend: { apiKey }
151
- deactivate Redis
152
- Backend->>DQM: GET /assets/{assetId}/<br/>pagehighlight/{checkpointId}<br/>x-api-key: {real_api_key}
153
- activate DQM
154
- DQM-->>Backend: HTML with .astError classes
155
- deactivate DQM
156
- Backend-->>Widget: Highlighted HTML
157
- deactivate Backend
158
- Widget->>Widget: Render in ShadowDOMRenderer<br/>with IntersectionObserver
159
- Widget-->>User: Show highlighted errors
160
- deactivate Widget
161
-
162
- Note over User,DQM: Security: API key never exposed to browser!<br/>All requests proxied through backend with session token.
163
- ```
164
-
165
- ### Key Concepts
166
-
167
- **🔐 Your OAuth Provider = Your Authentication System**
168
- - This is NOT Crownpeak OAuth
169
- - This is YOUR company's SSO system (Okta, Azure AD, Auth0, Keycloak, custom OAuth, etc.)
170
- - Users log in with THEIR work credentials
171
- - Example: `user@yourcompany.com` + password/MFA
172
-
173
- **🗄️ Your Database = DQM Credential Storage**
174
- - You store a mapping: `userId` → `{ dqmApiKey, dqmWebsiteId }`
175
- - When user logs in via SSO, backend looks up their DQM credentials
176
- - Users never see or enter Crownpeak API keys
177
- - Admin interface needed for initial credential setup
178
-
179
- **🎟️ Session Token = Temporary Access Key**
180
- - Backend creates this after successful SSO authentication
181
- - Widget uses this for all DQM requests
182
- - Maps to real DQM credentials server-side
183
- - Expires after 24 hours (configurable)
184
-
185
-
186
-
187
-
188
- ---
189
-
190
- ## Implementation Steps
191
-
192
- ### Step 1: Database Schema for User Credentials
193
-
194
- Create a table to store each user's DQM credentials:
195
-
196
- ```sql
197
- -- PostgreSQL Example
198
- CREATE TABLE user_dqm_credentials (
199
- user_id UUID PRIMARY KEY,
200
- api_key TEXT NOT NULL, -- ⚠️ MUST BE ENCRYPTED!
201
- website_id VARCHAR(255) NOT NULL,
202
- created_at TIMESTAMP DEFAULT NOW(),
203
- updated_at TIMESTAMP DEFAULT NOW(),
204
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
205
- );
206
-
207
- -- Add indexes for performance
208
- CREATE INDEX idx_user_dqm_user_id ON user_dqm_credentials(user_id);
209
-
210
- -- Audit trail (optional but recommended)
211
- CREATE TABLE dqm_credential_audit (
212
- id SERIAL PRIMARY KEY,
213
- user_id UUID NOT NULL,
214
- action VARCHAR(50) NOT NULL, -- 'created', 'updated', 'deleted', 'accessed'
215
- performed_by UUID, -- Admin user who made the change
216
- timestamp TIMESTAMP DEFAULT NOW(),
217
- FOREIGN KEY (user_id) REFERENCES users(id)
218
- );
219
- ```
220
-
221
- ⚠️ **CRITICAL SECURITY**: Always encrypt `api_key` before storing!
222
-
223
- ```typescript
224
- // Example encryption (using Node.js crypto)
225
- import crypto from 'crypto';
226
-
227
- const ENCRYPTION_KEY = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex'); // 32 bytes
228
- const ALGORITHM = 'aes-256-gcm';
229
-
230
- export function encrypt(text: string): string {
231
- const iv = crypto.randomBytes(16);
232
- const cipher = crypto.createCipheriv(ALGORITHM, ENCRYPTION_KEY, iv);
233
- let encrypted = cipher.update(text, 'utf8', 'hex');
234
- encrypted += cipher.final('hex');
235
- const authTag = cipher.getAuthTag();
236
- return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
237
- }
238
-
239
- export function decrypt(encrypted: string): string {
240
- const [ivHex, authTagHex, encryptedText] = encrypted.split(':');
241
- const iv = Buffer.from(ivHex, 'hex');
242
- const authTag = Buffer.from(authTagHex, 'hex');
243
- const decipher = crypto.createDecipheriv(ALGORITHM, ENCRYPTION_KEY, iv);
244
- decipher.setAuthTag(authTag);
245
- let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
246
- decrypted += decipher.final('utf8');
247
- return decrypted;
248
- }
249
-
250
- // Generate encryption key (run once, store in .env):
251
- // node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
252
- ```
253
-
254
- ### Step 2: Implement OAuth Callback Handler
255
-
256
- Edit `server/routes/auth.ts` and implement the callback handler:
257
-
258
- ```typescript
259
- import { Router, Request, Response } from 'express';
260
- import { sessionStore } from '../services/sessionStore.js';
261
- import { yourDB } from '../services/database.js'; // Your database service
262
- import { encrypt, decrypt } from '../utils/crypto.js'; // Your encryption utils
263
-
264
- const authRouter = Router();
265
-
266
- authRouter.post('/oauth2/callback', async (req: Request, res: Response) => {
267
- try {
268
- const { code, redirectUri } = req.body;
269
-
270
- if (!code || !redirectUri) {
271
- return res.status(400).json({
272
- error: true,
273
- message: 'Missing required parameters: code and redirectUri',
274
- });
275
- }
276
-
277
- // ============================================
278
- // STEP 1: Exchange code for access token
279
- // ============================================
280
- const tokenResponse = await fetch(process.env.OAUTH_TOKEN_URL!, {
281
- method: 'POST',
282
- headers: {
283
- 'Content-Type': 'application/x-www-form-urlencoded',
284
- },
285
- body: new URLSearchParams({
286
- grant_type: 'authorization_code',
287
- code,
288
- redirect_uri: redirectUri,
289
- client_id: process.env.OAUTH_CLIENT_ID!,
290
- client_secret: process.env.OAUTH_CLIENT_SECRET!,
291
- }),
292
- });
293
-
294
- if (!tokenResponse.ok) {
295
- const errorData = await tokenResponse.text();
296
- console.error('[Auth] Token exchange failed:', errorData);
297
- throw new Error('Failed to exchange authorization code for access token');
298
- }
299
-
300
- const tokenData = await tokenResponse.json();
301
- const accessToken = tokenData.access_token;
302
-
303
- // ============================================
304
- // STEP 2: Get user info from YOUR OAuth provider
305
- // ============================================
306
- const userInfoResponse = await fetch(process.env.OAUTH_USERINFO_URL!, {
307
- headers: {
308
- Authorization: `Bearer ${accessToken}`,
309
- },
310
- });
311
-
312
- if (!userInfoResponse.ok) {
313
- throw new Error('Failed to fetch user info from OAuth provider');
314
- }
315
-
316
- const userInfo = await userInfoResponse.json();
317
- const userId = userInfo.sub || userInfo.id || userInfo.userId;
318
-
319
- if (!userId) {
320
- throw new Error('User ID not found in OAuth response');
321
- }
322
-
323
- console.log(`[Auth] OAuth success for user: ${userId}`);
324
-
325
- // ============================================
326
- // STEP 3: Retrieve DQM credentials from YOUR database
327
- // ============================================
328
- const credentialsResult = await yourDB.query(
329
- 'SELECT api_key, website_id FROM user_dqm_credentials WHERE user_id = $1',
330
- [userId]
331
- );
332
-
333
- if (!credentialsResult || credentialsResult.rows.length === 0) {
334
- console.warn(`[Auth] No DQM credentials found for user ${userId}`);
335
- return res.status(404).json({
336
- error: true,
337
- message: 'DQM credentials not configured for this user. Please contact your administrator.',
338
- code: 'CREDENTIALS_NOT_FOUND',
339
- });
340
- }
341
-
342
- const { api_key: encryptedApiKey, website_id: websiteId } = credentialsResult.rows[0];
343
-
344
- // ============================================
345
- // STEP 4: Decrypt API key and create session
346
- // ============================================
347
- const apiKey = decrypt(encryptedApiKey);
348
-
349
- // Optional: Validate credentials with Crownpeak DQM API
350
- // const isValid = await validateDQMCredentials(apiKey, websiteId);
351
- // if (!isValid) {
352
- // return res.status(401).json({
353
- // error: true,
354
- // message: 'DQM credentials are invalid. Please contact your administrator.',
355
- // });
356
- // }
357
-
358
- // Create session in Redis/SessionStore
359
- const sessionToken = await sessionStore.create(apiKey, websiteId, userId);
360
-
361
- console.log(`[Auth] Created session ${sessionToken} for user ${userId}`);
362
-
363
- // ============================================
364
- // STEP 5: Return session token to widget
365
- // ============================================
366
- res.json({
367
- sessionToken,
368
- websiteId,
369
- userId, // Optional: for display purposes
370
- });
371
- } catch (error: any) {
372
- console.error('[Auth] OAuth2 callback error:', error);
373
- res.status(401).json({
374
- error: true,
375
- message: error.message || 'OAuth2 authentication failed',
376
- code: 'OAUTH_ERROR',
377
- });
378
- }
379
- });
380
-
381
- export { authRouter };
382
- ```
383
-
384
- ### Step 3: Configure Environment Variables
385
-
386
- Create `.env` file in your backend:
387
-
388
- ```bash
389
- # ==================================
390
- # YOUR OAuth/SSO Configuration
391
- # ==================================
392
- OAUTH_CLIENT_ID=your-oauth-client-id
393
- OAUTH_CLIENT_SECRET=your-oauth-client-secret
394
- OAUTH_AUTHORIZE_URL=https://your-oauth-provider.com/oauth2/authorize
395
- OAUTH_TOKEN_URL=https://your-oauth-provider.com/oauth2/token
396
- OAUTH_USERINFO_URL=https://your-oauth-provider.com/oauth2/userinfo
397
-
398
- # ==================================
399
- # DQM Backend Configuration
400
- # ==================================
401
- PORT=3001
402
- REDIS_URL=redis://localhost:6379
403
-
404
- # ==================================
405
- # Security
406
- # ==================================
407
- ENCRYPTION_KEY=your-32-byte-hex-key-here # Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
408
- SESSION_TTL=86400 # 24 hours in seconds
409
-
410
- # ==================================
411
- # CORS
412
- # ==================================
413
- CORS_ORIGINS=http://localhost:3000,https://yourdomain.com
414
-
415
- # ==================================
416
- # Database (example for PostgreSQL)
417
- # ==================================
418
- DATABASE_URL=postgresql://user:password@localhost:5432/yourdb
419
- ```
420
-
421
- ### Step 4: Configure Auth UI
422
-
423
- Edit `server-ui/.env`:
424
-
425
- ```bash
426
- # Point to YOUR OAuth provider
427
- VITE_OAUTH_CLIENT_ID=your-oauth-client-id
428
- VITE_OAUTH_AUTHORIZE_URL=https://your-oauth-provider.com/oauth2/authorize
429
- VITE_API_BASE_URL=http://localhost:3001
430
- ```
431
-
432
- ### Step 5: Admin Interface for Credential Management
433
-
434
- Create an admin interface where you (or admins) can configure DQM credentials for users:
435
-
436
- ```tsx
437
- // Example: Admin Settings Component
438
- import { useState, useEffect } from 'react';
439
-
440
- function UserDQMSettings({ userId }: { userId: string }) {
441
- const [apiKey, setApiKey] = useState('');
442
- const [websiteId, setWebsiteId] = useState('');
443
- const [loading, setLoading] = useState(false);
444
- const [error, setError] = useState('');
445
- const [success, setSuccess] = useState(false);
446
-
447
- // Load existing credentials
448
- useEffect(() => {
449
- fetch(`/api/admin/users/${userId}/dqm-credentials`, {
450
- headers: {
451
- Authorization: `Bearer ${yourAdminToken}`,
452
- },
453
- })
454
- .then((res) => res.json())
455
- .then((data) => {
456
- if (data.websiteId) {
457
- setWebsiteId(data.websiteId);
458
- // Don't load API key for security
459
- }
460
- });
461
- }, [userId]);
462
-
463
- const handleSave = async () => {
464
- setLoading(true);
465
- setError('');
466
- setSuccess(false);
467
-
468
- try {
469
- const response = await fetch(`/api/admin/users/${userId}/dqm-credentials`, {
470
- method: 'POST',
471
- headers: {
472
- 'Content-Type': 'application/json',
473
- Authorization: `Bearer ${yourAdminToken}`,
474
- },
475
- body: JSON.stringify({
476
- apiKey,
477
- websiteId,
478
- }),
479
- });
480
-
481
- if (!response.ok) {
482
- const errorData = await response.json();
483
- throw new Error(errorData.message || 'Failed to save credentials');
484
- }
485
-
486
- setSuccess(true);
487
- setApiKey(''); // Clear for security
488
- } catch (err: any) {
489
- setError(err.message);
490
- } finally {
491
- setLoading(false);
492
- }
493
- };
494
-
495
- return (
496
- <div className="dqm-settings-form">
497
- <h3>DQM Integration Settings</h3>
498
- <p>Configure Crownpeak DQM credentials for this user.</p>
499
-
500
- {error && <div className="error-message">{error}</div>}
501
- {success && <div className="success-message">Credentials saved successfully!</div>}
502
-
503
- <div className="form-group">
504
- <label htmlFor="apiKey">Crownpeak API Key</label>
505
- <input
506
- id="apiKey"
507
- type="password"
508
- value={apiKey}
509
- onChange={(e) => setApiKey(e.target.value)}
510
- placeholder="Enter new API key (leave blank to keep existing)"
511
- autoComplete="off"
512
- />
513
- <small>This will be encrypted before storage</small>
514
- </div>
515
-
516
- <div className="form-group">
517
- <label htmlFor="websiteId">Website ID</label>
518
- <input
519
- id="websiteId"
520
- value={websiteId}
521
- onChange={(e) => setWebsiteId(e.target.value)}
522
- placeholder="e.g., my-website-id"
523
- />
524
- </div>
525
-
526
- <button onClick={handleSave} disabled={loading}>
527
- {loading ? 'Saving...' : 'Save Credentials'}
528
- </button>
529
- </div>
530
- );
531
- }
532
-
533
- export default UserDQMSettings;
534
- ```
535
-
536
- Backend endpoint:
537
-
538
- ```typescript
539
- // Your backend admin API
540
- app.post('/api/admin/users/:userId/dqm-credentials',
541
- authenticateAdmin, // Middleware to ensure user is admin
542
- async (req, res) => {
543
- const { userId } = req.params;
544
- const { apiKey, websiteId } = req.body;
545
-
546
- if (!websiteId) {
547
- return res.status(400).json({ error: 'Website ID is required' });
548
- }
549
-
550
- try {
551
- // Optional: Validate credentials with Crownpeak before saving
552
- if (apiKey) {
553
- const isValid = await validateDQMCredentials(apiKey, websiteId);
554
- if (!isValid) {
555
- return res.status(400).json({
556
- error: 'Invalid DQM credentials. Please check API key and Website ID.'
557
- });
558
- }
559
-
560
- // Encrypt API key
561
- const encryptedKey = encrypt(apiKey);
562
-
563
- // Store in database
564
- await db.query(
565
- `INSERT INTO user_dqm_credentials (user_id, api_key, website_id, updated_at)
566
- VALUES ($1, $2, $3, NOW())
567
- ON CONFLICT (user_id)
568
- DO UPDATE SET api_key = $2, website_id = $3, updated_at = NOW()`,
569
- [userId, encryptedKey, websiteId]
570
- );
571
-
572
- // Audit trail
573
- await db.query(
574
- `INSERT INTO dqm_credential_audit (user_id, action, performed_by, timestamp)
575
- VALUES ($1, 'updated', $2, NOW())`,
576
- [userId, req.user.id]
577
- );
578
- } else {
579
- // Update only website ID (keep existing API key)
580
- await db.query(
581
- `UPDATE user_dqm_credentials
582
- SET website_id = $1, updated_at = NOW()
583
- WHERE user_id = $2`,
584
- [websiteId, userId]
585
- );
586
- }
587
-
588
- res.json({ success: true });
589
- } catch (error: any) {
590
- console.error('[Admin] Failed to save DQM credentials:', error);
591
- res.status(500).json({
592
- error: 'Failed to save credentials',
593
- message: error.message
594
- });
595
- }
596
- }
597
- );
598
-
599
- async function validateDQMCredentials(apiKey: string, websiteId: string): Promise<boolean> {
600
- try {
601
- const response = await fetch(
602
- `https://api.crownpeak.net/dqm-cms/v1/websites/${websiteId}`,
603
- {
604
- headers: {
605
- 'x-api-key': apiKey,
606
- },
607
- }
608
- );
609
- return response.ok;
610
- } catch {
611
- return false;
612
- }
613
- }
614
- ```
615
-
616
- ### Step 6: Widget Configuration
617
-
618
- In your React app where you embed the DQM Widget:
619
-
620
- ```tsx
621
- import { DQMSidebar } from '@crownpeak/dqm-react-component';
622
- import { useState, useEffect } from 'react';
623
-
624
- function MyApp() {
625
- const [isDQMOpen, setIsDQMOpen] = useState(false);
626
-
627
- const handleOpenDQM = () => {
628
- // Check if user already has a session token
629
- const existingToken = localStorage.getItem('dqm_sessionToken');
630
- const sessionType = localStorage.getItem('dqm_sessionType');
631
-
632
- if (existingToken && sessionType === 'backend') {
633
- // User is already authenticated, open widget directly
634
- setIsDQMOpen(true);
635
- } else {
636
- // Open SSO login page in popup
637
- const authWindow = window.open(
638
- 'http://localhost:3001/auth/login',
639
- 'DQM Login',
640
- 'width=500,height=700,scrollbars=yes,resizable=yes'
641
- );
642
-
643
- if (!authWindow) {
644
- alert('Please enable popups to log in');
645
- return;
646
- }
647
-
648
- // Listen for authentication success
649
- const handleMessage = (event: MessageEvent) => {
650
- // Verify origin for security
651
- if (event.origin !== 'http://localhost:3001') {
652
- return;
653
- }
654
-
655
- if (event.data.type === 'DQM_AUTH_SUCCESS') {
656
- const { sessionToken, websiteId } = event.data;
657
-
658
- // Store session token in localStorage
659
- localStorage.setItem('dqm_sessionToken', sessionToken);
660
- localStorage.setItem('dqm_websiteID', websiteId);
661
- localStorage.setItem('dqm_sessionType', 'backend');
662
-
663
- // Close auth window
664
- authWindow.close();
665
-
666
- // Open widget
667
- setIsDQMOpen(true);
668
-
669
- // Remove event listener
670
- window.removeEventListener('message', handleMessage);
671
- } else if (event.data.type === 'DQM_AUTH_ERROR') {
672
- alert('Authentication failed: ' + event.data.error);
673
- authWindow.close();
674
- window.removeEventListener('message', handleMessage);
675
- }
676
- };
677
-
678
- window.addEventListener('message', handleMessage);
679
- }
680
- };
681
-
682
- return (
683
- <div className="app">
684
- <header>
685
- <h1>My Application</h1>
686
- <button onClick={handleOpenDQM} className="dqm-button">
687
- ✨ Check Quality with DQM
688
- </button>
689
- </header>
690
-
691
- <main>
692
- {/* Your app content */}
693
- </main>
694
-
695
- <DQMSidebar
696
- open={isDQMOpen}
697
- onClose={() => setIsDQMOpen(false)}
698
- onOpen={() => setIsDQMOpen(true)}
699
- config={{
700
- backendUrl: 'http://localhost:3001', // Your backend proxy
701
- useBackend: true, // Enable backend mode
702
- useLocalStorage: true, // Store session token
703
- }}
704
- onAuthSuccess={(credentials) => {
705
- console.log('DQM authenticated:', credentials.sessionType);
706
- }}
707
- onAuthError={(error) => {
708
- console.error('DQM auth error:', error);
709
- // Clear invalid session
710
- localStorage.removeItem('dqm_sessionToken');
711
- localStorage.removeItem('dqm_websiteID');
712
- localStorage.removeItem('dqm_sessionType');
713
- }}
714
- />
715
- </div>
716
- );
717
- }
718
-
719
- export default MyApp;
720
- ```
721
-
722
-
723
-
724
-
725
- ---
726
-
727
- ## API Endpoints Reference
728
-
729
- Your backend must implement these endpoints for the widget to communicate with Crownpeak DQM API.
730
-
731
- ### Authentication Endpoints
732
-
733
- #### 1. Direct Credentials Login (Fallback Mode)
734
-
735
- **Endpoint:** `POST /auth/login`
736
-
737
- **Description:** User provides API Key and Website ID directly. Backend validates and issues session token.
738
-
739
- **Request:**
740
- ```json
741
- {
742
- "apiKey": "user_crownpeak_api_key",
743
- "websiteId": "user_website_id"
744
- }
745
- ```
746
-
747
- **Response (Success - 200):**
748
- ```json
749
- {
750
- "sessionToken": "unique_session_token_or_jwt",
751
- "websiteId": "user_website_id"
752
- }
753
- ```
754
-
755
- **Response (Error - 401):**
756
- ```json
757
- {
758
- "error": true,
759
- "message": "Invalid credentials or authentication failed"
760
- }
761
- ```
762
-
763
- #### 2. Backend Session Authentication
764
-
765
- **Endpoint:** `POST /auth/token`
766
-
767
- **Description:** User already authenticated via your backend's session/cookie. Backend issues DQM session token.
768
-
769
- **Headers:**
770
- ```
771
- Cookie: your_session_cookie
772
- ```
773
-
774
- **Response (Success - 200):**
775
- ```json
776
- {
777
- "sessionToken": "unique_session_token",
778
- "websiteId": "user_website_id"
779
- }
780
- ```
781
-
782
- **Response (Error - 401):**
783
- ```json
784
- {
785
- "error": true,
786
- "message": "Not authenticated"
787
- }
788
- ```
789
-
790
- #### 3. OAuth2 Callback (Recommended for SSO)
791
-
792
- **Endpoint:** `POST /auth/oauth2/callback`
793
-
794
- **Description:** Process OAuth2 authorization code and issue session token.
795
-
796
- **Request:**
797
- ```json
798
- {
799
- "code": "oauth2_authorization_code",
800
- "redirectUri": "https://your-backend.com/auth/callback"
801
- }
802
- ```
803
-
804
- **Response (Success - 200):**
805
- ```json
806
- {
807
- "sessionToken": "unique_session_token",
808
- "websiteId": "user_website_id",
809
- "userId": "user123" // Optional
810
- }
811
- ```
812
-
813
- **Response (Error - 401):**
814
- ```json
815
- {
816
- "error": true,
817
- "message": "OAuth2 token exchange failed",
818
- "code": "OAUTH_ERROR"
819
- }
820
- ```
821
-
822
- **Response (Error - 404):**
823
- ```json
824
- {
825
- "error": true,
826
- "message": "DQM credentials not configured for this user. Please contact your administrator.",
827
- "code": "CREDENTIALS_NOT_FOUND"
828
- }
829
- ```
830
-
831
- ---
832
-
833
- ### DQM Proxy Endpoints
834
-
835
- All DQM API calls are proxied through your backend when in Backend Mode.
836
-
837
- #### 4. Start HTML Analysis
838
-
839
- **Endpoint:** `POST /dqm/assets`
840
-
841
- **Description:** Proxy for Crownpeak DQM API - Start HTML analysis.
842
-
843
- **Headers:**
844
- ```
845
- Authorization: Bearer <sessionToken>
846
- Content-Type: application/json
847
- ```
848
-
849
- **Request Body:**
850
- ```json
851
- {
852
- "html": "<html>...</html>",
853
- "url": "https://example.com/page",
854
- "websiteId": "optional_if_backend_knows"
855
- }
856
- ```
857
-
858
- **Response (Success - 200):**
859
- ```json
860
- {
861
- "assetId": "generated_asset_id",
862
- "analysisState": "analyzing",
863
- "totalCheckpoints": 150
864
- }
865
- ```
866
-
867
- **Backend Implementation:**
868
- ```typescript
869
- app.post('/dqm/assets', authenticateSession, async (req, res) => {
870
- const { apiKey, websiteId } = req.session; // From session store
871
-
872
- const response = await fetch('https://api.crownpeak.net/dqm-cms/v1/assets', {
873
- method: 'POST',
874
- headers: {
875
- 'Content-Type': 'application/json',
876
- 'x-api-key': apiKey,
877
- },
878
- body: JSON.stringify({
879
- ...req.body,
880
- websiteId, // Ensure correct websiteId
881
- }),
882
- });
883
-
884
- const data = await response.json();
885
- res.json(data);
886
- });
887
- ```
888
-
889
- #### 5. Get Analysis Status (Polling)
890
-
891
- **Endpoint:** `GET /dqm/assets/:assetId`
892
-
893
- **Description:** Poll analysis status and get results when complete.
894
-
895
- **Headers:**
896
- ```
897
- Authorization: Bearer <sessionToken>
898
- ```
899
-
900
- **Response (Success - 200):**
901
- ```json
902
- {
903
- "assetId": "asset_id",
904
- "checkpoints": [
905
- {
906
- "checkpointId": "cp1",
907
- "name": "Heading Structure",
908
- "category": "Accessibility",
909
- "severity": "high",
910
- "failedCount": 3,
911
- "passedCount": 12,
912
- "canHighlight": { "page": true, "source": true }
913
- }
914
- ],
915
- "totalCheckpoints": 150,
916
- "totalErrors": 42
917
- }
918
- ```
919
-
920
- **Backend Implementation:**
921
- ```typescript
922
- app.get('/dqm/assets/:assetId', authenticateSession, async (req, res) => {
923
- const { apiKey } = req.session;
924
- const { assetId } = req.params;
925
-
926
- const response = await fetch(
927
- `https://api.crownpeak.net/dqm-cms/v1/assets/${assetId}?apiKey=${apiKey}`,
928
- {
929
- headers: { 'x-api-key': apiKey },
930
- }
931
- );
932
-
933
- const data = await response.json();
934
- res.json(data);
935
- });
936
- ```
937
-
938
- #### 6. Get Highlighted HTML (All Errors)
939
-
940
- **Endpoint:** `GET /dqm/assets/:assetId/pagehighlight/all`
941
-
942
- **Description:** Get HTML with all errors highlighted using CSS classes.
943
-
944
- **Headers:**
945
- ```
946
- Authorization: Bearer <sessionToken>
947
- ```
948
-
949
- **Response (Success - 200):**
950
- ```html
951
- <html>
952
- <body>
953
- <h1 class="astHighlightFull astError" data-checkpoint="cp1">
954
- Heading text
955
- </h1>
956
- <!-- More highlighted HTML -->
957
- </body>
958
- </html>
959
- ```
960
-
961
- **Backend Implementation:**
962
- ```typescript
963
- app.get('/dqm/assets/:assetId/pagehighlight/all', authenticateSession, async (req, res) => {
964
- const { apiKey } = req.session;
965
- const { assetId } = req.params;
966
-
967
- const response = await fetch(
968
- `https://api.crownpeak.net/dqm-cms/v1/assets/${assetId}/pagehighlight/all?apiKey=${apiKey}`,
969
- {
970
- headers: { 'x-api-key': apiKey },
971
- }
972
- );
973
-
974
- const html = await response.text();
975
- res.setHeader('Content-Type', 'text/html');
976
- res.send(html);
977
- });
978
- ```
979
-
980
- #### 7. Get Highlighted HTML (Specific Checkpoint)
981
-
982
- **Endpoint:** `GET /dqm/assets/:assetId/pagehighlight/:checkpointId`
983
-
984
- **Description:** Get HTML with only specific checkpoint errors highlighted.
985
-
986
- **Headers:**
987
- ```
988
- Authorization: Bearer <sessionToken>
989
- ```
990
-
991
- **Response (Success - 200):**
992
- ```html
993
- <html>
994
- <body>
995
- <h1 class="astHighlightStart astError">Heading</h1>
996
- </body>
997
- </html>
998
- ```
999
-
1000
- **Backend Implementation:**
1001
- ```typescript
1002
- app.get('/dqm/assets/:assetId/pagehighlight/:checkpointId', authenticateSession, async (req, res) => {
1003
- const { apiKey } = req.session;
1004
- const { assetId, checkpointId } = req.params;
1005
-
1006
- const response = await fetch(
1007
- `https://api.crownpeak.net/dqm-cms/v1/assets/${assetId}/pagehighlight/${checkpointId}?apiKey=${apiKey}`,
1008
- {
1009
- headers: { 'x-api-key': apiKey },
1010
- }
1011
- );
1012
-
1013
- const html = await response.text();
1014
- res.setHeader('Content-Type', 'text/html');
1015
- res.send(html);
1016
- });
1017
- ```
1018
-
1019
- ---
1020
-
1021
- ## Session Management
1022
-
1023
- ### Session Store Interface
1024
-
1025
- ```typescript
1026
- interface Session {
1027
- apiKey: string; // Real Crownpeak API key
1028
- websiteId: string; // Website ID
1029
- userId?: string; // Your user identifier
1030
- expiresAt: number; // Unix timestamp
1031
- }
1032
-
1033
- interface SessionStore {
1034
- create(apiKey: string, websiteId: string, userId?: string): Promise<string>;
1035
- get(token: string): Promise<Session | null>;
1036
- delete(token: string): Promise<void>;
1037
- refresh(token: string): Promise<void>;
1038
- }
1039
- ```
1040
-
1041
- ### Redis Implementation (Recommended)
1042
-
1043
- ```typescript
1044
- import Redis from 'ioredis';
1045
- import crypto from 'crypto';
1046
-
1047
- const redis = new Redis(process.env.REDIS_URL);
1048
- const SESSION_TTL = 86400; // 24 hours
1049
-
1050
- export const sessionStore = {
1051
- async create(apiKey: string, websiteId: string, userId?: string): Promise<string> {
1052
- const token = crypto.randomBytes(32).toString('hex');
1053
- const session: Session = {
1054
- apiKey,
1055
- websiteId,
1056
- userId,
1057
- expiresAt: Date.now() + SESSION_TTL * 1000,
1058
- };
1059
-
1060
- await redis.setex(
1061
- `session:${token}`,
1062
- SESSION_TTL,
1063
- JSON.stringify(session)
1064
- );
1065
-
1066
- return token;
1067
- },
1068
-
1069
- async get(token: string): Promise<Session | null> {
1070
- const data = await redis.get(`session:${token}`);
1071
- if (!data) return null;
1072
-
1073
- const session: Session = JSON.parse(data);
1074
-
1075
- // Check expiration
1076
- if (session.expiresAt < Date.now()) {
1077
- await this.delete(token);
1078
- return null;
1079
- }
1080
-
1081
- return session;
1082
- },
1083
-
1084
- async delete(token: string): Promise<void> {
1085
- await redis.del(`session:${token}`);
1086
- },
1087
-
1088
- async refresh(token: string): Promise<void> {
1089
- const session = await this.get(token);
1090
- if (session) {
1091
- session.expiresAt = Date.now() + SESSION_TTL * 1000;
1092
- await redis.setex(
1093
- `session:${token}`,
1094
- SESSION_TTL,
1095
- JSON.stringify(session)
1096
- );
1097
- }
1098
- },
1099
- };
1100
- ```
1101
-
1102
- ### Authentication Middleware
1103
-
1104
- ```typescript
1105
- import { Request, Response, NextFunction } from 'express';
1106
-
1107
- interface AuthenticatedRequest extends Request {
1108
- session?: {
1109
- apiKey: string;
1110
- websiteId: string;
1111
- userId?: string;
1112
- };
1113
- }
1114
-
1115
- export async function authenticateSession(
1116
- req: AuthenticatedRequest,
1117
- res: Response,
1118
- next: NextFunction
1119
- ) {
1120
- try {
1121
- // Extract token from Authorization header
1122
- const authHeader = req.headers.authorization;
1123
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
1124
- return res.status(401).json({
1125
- error: true,
1126
- message: 'Missing or invalid Authorization header',
1127
- });
1128
- }
1129
-
1130
- const token = authHeader.substring(7); // Remove 'Bearer '
1131
-
1132
- // Get session from store
1133
- const session = await sessionStore.get(token);
1134
- if (!session) {
1135
- return res.status(401).json({
1136
- error: true,
1137
- message: 'Invalid or expired session token',
1138
- });
1139
- }
1140
-
1141
- // Attach session to request
1142
- req.session = session;
1143
-
1144
- // Optional: Refresh session TTL on activity
1145
- await sessionStore.refresh(token);
1146
-
1147
- next();
1148
- } catch (error) {
1149
- console.error('[Auth] Session verification failed:', error);
1150
- res.status(500).json({
1151
- error: true,
1152
- message: 'Internal server error during authentication',
1153
- });
1154
- }
1155
- }
1156
- ```
1157
-
1158
- ---
1159
-
1160
- ## Security Best Practices
1161
-
1162
- ### 1. Encryption at Rest
1163
-
1164
- Always encrypt API keys in database:
1165
-
1166
- ```typescript
1167
- // Generate encryption key (run once):
1168
- // node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
1169
-
1170
- // Store in .env:
1171
- ENCRYPTION_KEY=your-64-character-hex-string-here
1172
- ```
1173
-
1174
- ### 2. HTTPS Only
1175
-
1176
- ```typescript
1177
- // Redirect HTTP to HTTPS in production
1178
- app.use((req, res, next) => {
1179
- if (process.env.NODE_ENV === 'production' && !req.secure) {
1180
- return res.redirect(`https://${req.headers.host}${req.url}`);
1181
- }
1182
- next();
1183
- });
1184
- ```
1185
-
1186
- ### 3. CORS Configuration
1187
-
1188
- ```typescript
1189
- import cors from 'cors';
1190
-
1191
- const allowedOrigins = process.env.CORS_ORIGINS?.split(',') || [];
1192
-
1193
- app.use(cors({
1194
- origin: (origin, callback) => {
1195
- if (!origin || allowedOrigins.includes(origin)) {
1196
- callback(null, true);
1197
- } else {
1198
- callback(new Error('Not allowed by CORS'));
1199
- }
1200
- },
1201
- credentials: true,
1202
- }));
1203
- ```
1204
-
1205
- ### 4. Rate Limiting
1206
-
1207
- ```typescript
1208
- import rateLimit from 'express-rate-limit';
1209
-
1210
- const authLimiter = rateLimit({
1211
- windowMs: 15 * 60 * 1000, // 15 minutes
1212
- max: 5, // 5 requests per window
1213
- message: 'Too many authentication attempts, please try again later',
1214
- });
1215
-
1216
- app.use('/auth', authLimiter);
1217
- ```
1218
-
1219
- ### 5. Input Validation
1220
-
1221
- ```typescript
1222
- import { body, validationResult } from 'express-validator';
1223
-
1224
- app.post('/auth/login',
1225
- body('apiKey').isString().trim().notEmpty(),
1226
- body('websiteId').isString().trim().notEmpty(),
1227
- async (req, res) => {
1228
- const errors = validationResult(req);
1229
- if (!errors.isEmpty()) {
1230
- return res.status(400).json({ errors: errors.array() });
1231
- }
1232
- // ... proceed with login
1233
- }
1234
- );
1235
- ```
1236
-
1237
- ### 6. Audit Logging
1238
-
1239
- ```typescript
1240
- async function logAuthEvent(
1241
- userId: string,
1242
- action: string,
1243
- success: boolean,
1244
- ipAddress: string
1245
- ) {
1246
- await db.query(
1247
- `INSERT INTO auth_audit_log (user_id, action, success, ip_address, timestamp)
1248
- VALUES ($1, $2, $3, $4, NOW())`,
1249
- [userId, action, success, ipAddress]
1250
- );
1251
- }
1252
-
1253
- // Usage:
1254
- await logAuthEvent(userId, 'oauth_login', true, req.ip);
1255
- ```
1256
-
1257
- ---
1258
-
1259
- ## Alternative: Shared Organizational Credentials
1260
-
1261
- If you don't want per-user credentials, use a **single organization-wide API key**:
1262
-
1263
- ```typescript
1264
- // All users share same DQM credentials
1265
- authRouter.post('/oauth2/callback', async (req, res) => {
1266
- // ... OAuth validation ...
1267
-
1268
- // Use organization-wide credentials from environment
1269
- const apiKey = process.env.DQM_ORG_API_KEY!;
1270
- const websiteId = process.env.DQM_ORG_WEBSITE_ID!;
1271
-
1272
- const sessionToken = await sessionStore.create(apiKey, websiteId, userId);
1273
-
1274
- res.json({ sessionToken, websiteId });
1275
- });
1276
- ```
1277
-
1278
- **Pros:**
1279
- - ✅ Simpler setup - no per-user credential management
1280
- - ✅ No database schema needed
1281
- - ✅ Easier admin maintenance
1282
-
1283
- **Cons:**
1284
- - ❌ All users share same DQM analysis context
1285
- - ❌ Cannot track usage per user
1286
- - ❌ Cannot have different permissions per user
1287
-
1288
- ---
1289
-
1290
- ## Testing
1291
-
1292
- ### Test OAuth Flow
1293
-
1294
- ```bash
1295
- # 1. Start backend
1296
- npm run start:server
1297
-
1298
- # 2. Open login page
1299
- open http://localhost:3001/auth/login
1300
-
1301
- # 3. Click "Sign in with SSO"
1302
- # Should redirect to your OAuth provider
1303
-
1304
- # 4. After auth, check callback
1305
- # Should exchange code for token
1306
- # Should create session in Redis
1307
- # Should return sessionToken
1308
- ```
1309
-
1310
- ### Test Direct Login
1311
-
1312
- ```bash
1313
- curl -X POST http://localhost:3001/auth/login \
1314
- -H "Content-Type: application/json" \
1315
- -d '{
1316
- "apiKey": "your_test_api_key",
1317
- "websiteId": "your_test_website_id"
1318
- }'
1319
-
1320
- # Expected response:
1321
- # {"sessionToken":"abc123...","websiteId":"your_test_website_id"}
1322
- ```
1323
-
1324
- ### Test DQM Proxy
1325
-
1326
- ```bash
1327
- # Get session token first
1328
- TOKEN="your_session_token_here"
1329
-
1330
- # Start analysis
1331
- curl -X POST http://localhost:3001/dqm/assets \
1332
- -H "Authorization: Bearer $TOKEN" \
1333
- -H "Content-Type: application/json" \
1334
- -d '{
1335
- "html": "<html><body><h1>Test</h1></body></html>",
1336
- "url": "https://example.com"
1337
- }'
1338
-
1339
- # Poll status
1340
- curl -H "Authorization: Bearer $TOKEN" \
1341
- http://localhost:3001/dqm/assets/ASSET_ID_HERE
1342
- ```
1343
-
1344
- ---
1345
-
1346
- ## Deployment Checklist
1347
-
1348
- - [ ] Implement OAuth callback handler in `server/routes/auth.ts`
1349
- - [ ] Create database schema for `user_dqm_credentials`
1350
- - [ ] Generate encryption key and store in environment
1351
- - [ ] Configure OAuth provider (client ID, secret, URLs)
1352
- - [ ] Set up Redis (local, Redis Cloud, AWS ElastiCache)
1353
- - [ ] Build all components: `npm run build`
1354
- - [ ] Configure CORS for production domains
1355
- - [ ] Enable HTTPS (Let's Encrypt, AWS Certificate Manager)
1356
- - [ ] Add rate limiting to auth endpoints
1357
- - [ ] Set up audit logging
1358
- - [ ] Test complete SSO flow end-to-end
1359
- - [ ] Test direct login fallback
1360
- - [ ] Monitor backend logs for errors
1361
- - [ ] Document process for users/admins
1362
-
1363
- ---
1364
-
1365
- ## Benefits of Backend Mode
1366
-
1367
- ✅ **Security**
1368
- - API keys never exposed in browser
1369
- - Encrypted storage in database
1370
- - Server-side request signing
1371
- - Audit trail of all requests
1372
-
1373
- ✅ **Centralized Management**
1374
- - Admin can update credentials for all users
1375
- - Single point for credential rotation
1376
- - Per-user or organization-wide credentials
1377
- - Consistent credential lifecycle
1378
-
1379
- ✅ **SSO Integration**
1380
- - Seamless login with existing user system
1381
- - No separate credential management for users
1382
- - Consistent UX with your application
1383
- - Support for enterprise identity providers
1384
-
1385
- ✅ **Scalability**
1386
- - Redis session store for high availability
1387
- - Horizontal scaling support
1388
- - Session sharing across instances
1389
- - Automatic session cleanup
1390
-
1391
- ✅ **Compliance**
1392
- - Meet security requirements
1393
- - Data encryption at rest and in transit
1394
- - Access control and permissions
1395
- - Detailed audit logs
1396
-
1397
- ---
1398
-
1399
- ## Support & Resources
1400
-
1401
- - **Crownpeak DQM API**: https://docs.crownpeak.com/dqm-api
1402
- - **Component Source**: `src/DQMSidebar.tsx`
1403
- - **Auth UI Source**: `server-ui/src/`
1404
- - **Backend Source**: `server/`
1405
- - **Examples**: See `EXAMPLES.md` for integration patterns
1406
-
1407
-
1408
-
1409
- ---
1410
-
1411
- ### 2. Backend Session Authentication
1412
-
1413
- **Endpoint:** `POST /auth/token`
1414
-
1415
- **Description:** User is already authenticated via your backend's session/cookie system. Backend issues a DQM session token based on current user session.
1416
-
1417
- **Request:**
1418
- ```json
1419
- {}
1420
- ```
1421
-
1422
- **Headers:**
1423
- ```
1424
- Cookie: your_session_cookie
1425
- ```
1426
-
1427
- **Response (Success - 200):**
1428
- ```json
1429
- {
1430
- "sessionToken": "unique_session_token_or_jwt",
1431
- "websiteId": "user_website_id" // Optional
1432
- }
1433
- ```
1434
-
1435
- **Response (Error - 401):**
1436
- ```json
1437
- {
1438
- "message": "Not authenticated"
1439
- }
1440
- ```
1441
-
1442
- **Notes:**
1443
- - Uses existing user session (cookies, JWT, etc.)
1444
- - Backend determines user identity and associated DQM credentials
1445
- - Useful when user is already logged into your platform
1446
-
1447
- ---
1448
-
1449
- ### 3. OAuth2 Callback
1450
-
1451
- **Endpoint:** `POST /auth/oauth2/callback`
1452
-
1453
- **Description:** Process OAuth2 authorization code and issue session token.
1454
-
1455
- **Request:**
1456
- ```json
1457
- {
1458
- "code": "oauth2_authorization_code",
1459
- "redirectUri": "https://your-app.com/oauth-callback"
1460
- }
1461
- ```
1462
-
1463
- **Response (Success - 200):**
1464
- ```json
1465
- {
1466
- "sessionToken": "unique_session_token_or_jwt",
1467
- "websiteId": "user_website_id" // Optional
1468
- }
1469
- ```
1470
-
1471
- **Response (Error - 401):**
1472
- ```json
1473
- {
1474
- "message": "OAuth2 token exchange failed"
1475
- }
1476
- ```
1477
-
1478
- **Notes:**
1479
- - Backend exchanges authorization code for access token with OAuth2 provider
1480
- - Retrieves user's DQM credentials from OAuth2 provider or your database
1481
- - Issues session token for DQM component
1482
-
1483
- ---
1484
-
1485
- ## DQM API Proxy Endpoints
1486
-
1487
- When in Backend Mode, **ALL** DQM API calls are proxied through your backend. The component will use `config.authBackendUrl` as the base URL instead of calling Crownpeak DQM API directly.
1488
-
1489
- ### 4. Start Analysis
1490
-
1491
- **Endpoint:** `POST /dqm/assets`
1492
-
1493
- **Description:** Proxy for Crownpeak DQM API `POST /assets` - Start HTML analysis.
1494
-
1495
- **Request Headers:**
1496
- ```
1497
- Authorization: Bearer <sessionToken>
1498
- Content-Type: application/json
1499
- ```
1500
-
1501
- **Request Body:**
1502
- ```json
1503
- {
1504
- "html": "<html>...</html>",
1505
- "url": "https://example.com/page",
1506
- "websiteId": "optional_if_backend_knows"
1507
- }
1508
- ```
1509
-
1510
- **Response (Success - 200):**
1511
- ```json
1512
- {
1513
- "assetId": "generated_asset_id",
1514
- "analysisState": "analyzing"
1515
- }
1516
- ```
1517
-
1518
- **Backend Implementation:**
1519
- 1. Verify session token and retrieve real API key + websiteId
1520
- 2. Call Crownpeak DQM API: `POST https://api.crownpeak.net/dqm-cms/v1/assets`
1521
- - Headers: `x-api-key: <real_api_key>`
1522
- - Body: Same as client request
1523
- 3. Return response to client
1524
-
1525
- ---
1526
-
1527
- ### 5. Get Analysis Status
1528
-
1529
- **Endpoint:** `GET /dqm/assets/{assetId}`
1530
-
1531
- **Description:** Proxy for Crownpeak DQM API `GET /assets/{assetId}` - Poll analysis status.
1532
-
1533
- **Request Headers:**
1534
- ```
1535
- Authorization: Bearer <sessionToken>
1536
- ```
1537
-
1538
- **Response (Success - 200):**
1539
- ```json
1540
- {
1541
- "assetId": "asset_id",
1542
- "analysisState": "completed",
1543
- "checkpoints": [...],
1544
- "totalErrors": 42,
1545
- // ... full analysis data
1546
- }
1547
- ```
1548
-
1549
- **Backend Implementation:**
1550
- 1. Verify session token and retrieve real API key
1551
- 2. Call Crownpeak DQM API: `GET https://api.crownpeak.net/dqm-cms/v1/assets/{assetId}?apiKey=<real_api_key>`
1552
- 3. Return response to client
1553
-
1554
- ---
1555
-
1556
- ### 6. Get Highlighted HTML (All Errors)
1557
-
1558
- **Endpoint:** `GET /dqm/assets/{assetId}/pagehighlight/all`
1559
-
1560
- **Description:** Proxy for Crownpeak DQM API - Get HTML with all errors highlighted.
1561
-
1562
- **Request Headers:**
1563
- ```
1564
- Authorization: Bearer <sessionToken>
1565
- ```
1566
-
1567
- **Response (Success - 200):**
1568
- ```html
1569
- <html>
1570
- <!-- HTML with .astHighlightFull, .astError classes -->
1571
- </html>
1572
- ```
1573
-
1574
- **Backend Implementation:**
1575
- 1. Verify session token and retrieve real API key
1576
- 2. Call Crownpeak DQM API: `GET https://api.crownpeak.net/dqm-cms/v1/assets/{assetId}/pagehighlight/all?apiKey=<real_api_key>`
1577
- 3. Return HTML response to client
1578
-
1579
- ---
1580
-
1581
- ### 7. Get Highlighted HTML (Specific Checkpoint)
1582
-
1583
- **Endpoint:** `GET /dqm/assets/{assetId}/pagehighlight/{checkpointId}`
1584
-
1585
- **Description:** Proxy for Crownpeak DQM API - Get HTML with specific checkpoint errors highlighted.
1586
-
1587
- **Request Headers:**
1588
- ```
1589
- Authorization: Bearer <sessionToken>
1590
- ```
1591
-
1592
- **Response (Success - 200):**
1593
- ```html
1594
- <html>
1595
- <!-- HTML with .astHighlightFull, .astError classes for specific checkpoint -->
1596
- </html>
1597
- ```
1598
-
1599
- **Backend Implementation:**
1600
- 1. Verify session token and retrieve real API key
1601
- 2. Call Crownpeak DQM API: `GET https://api.crownpeak.net/dqm-cms/v1/assets/{assetId}/pagehighlight/{checkpointId}?apiKey=<real_api_key>`
1602
- 3. Return HTML response to client
1603
-
1604
- ---
1605
-
1606
- ## Session Management
1607
-
1608
- ### Session Token Storage
1609
-
1610
- **Client-Side (localStorage):**
1611
- ```javascript
1612
- localStorage.setItem('dqm_sessionToken', 'token');
1613
- localStorage.setItem('dqm_sessionType', 'backend');
1614
- ```
1615
-
1616
- **Backend-Side:**
1617
- - Store mapping: `sessionToken -> { apiKey, websiteId, userId, expiresAt }`
1618
- - Recommended: Redis, database, or in-memory cache with TTL
1619
- - Session should expire after inactivity (e.g., 24 hours)
1620
-
1621
- ### Token Validation
1622
-
1623
- On every proxied API request:
1624
- 1. Extract `Authorization: Bearer <sessionToken>` header
1625
- 2. Validate session token (check existence, expiration)
1626
- 3. Retrieve associated API key and websiteId
1627
- 4. Proxy request to Crownpeak DQM API with real credentials
1628
-
1629
- ---
1630
-
1631
- ## Error Handling
1632
-
1633
- ### Standard Error Response
1634
-
1635
- ```json
1636
- {
1637
- "error": true,
1638
- "message": "Human-readable error message",
1639
- "code": "ERROR_CODE" // Optional
1640
- }
1641
- ```
1642
-
1643
- ### Common HTTP Status Codes
1644
-
1645
- - `200` - Success
1646
- - `400` - Bad Request (invalid input)
1647
- - `401` - Unauthorized (invalid session token or credentials)
1648
- - `403` - Forbidden (valid token but insufficient permissions)
1649
- - `500` - Internal Server Error
1650
-
1651
- ---
1652
-
1653
- ## Security Considerations
1654
-
1655
- 1. **HTTPS Only:** Always use HTTPS in production
1656
- 2. **CORS:** Configure CORS to allow requests from your React app domain
1657
- 3. **Rate Limiting:** Implement rate limiting on auth endpoints
1658
- 4. **Session Expiration:** Sessions should expire after 24 hours or on logout
1659
- 5. **Token Rotation:** Consider refreshing session tokens periodically
1660
- 6. **CSRF Protection:** Use CSRF tokens for state-changing operations
1661
- 7. **Input Validation:** Validate all inputs (API keys, HTML content, etc.)
1662
- 8. **API Key Storage:** Store real API keys encrypted in database
1663
-
1664
- ---
1665
-
1666
- ## Example Backend Implementation (Node.js/Express)
1667
-
1668
- ```javascript
1669
- const express = require('express');
1670
- const axios = require('axios');
1671
- const app = express();
1672
-
1673
- // Session store (use Redis in production)
1674
- const sessions = new Map();
1675
-
1676
- // Auth endpoint - Direct credentials
1677
- app.post('/auth/login', async (req, res) => {
1678
- const { apiKey, websiteId } = req.body;
1679
-
1680
- // Validate credentials with Crownpeak API
1681
- try {
1682
- const response = await axios.get(
1683
- `https://api.crownpeak.net/dqm-cms/v1/websites/${websiteId}`,
1684
- { headers: { 'x-api-key': apiKey } }
1685
- );
1686
-
1687
- // Generate session token
1688
- const sessionToken = generateUniqueToken();
1689
- sessions.set(sessionToken, {
1690
- apiKey,
1691
- websiteId,
1692
- expiresAt: Date.now() + 24 * 60 * 60 * 1000, // 24 hours
1693
- });
1694
-
1695
- res.json({ sessionToken, websiteId });
1696
- } catch (error) {
1697
- res.status(401).json({ message: 'Invalid credentials' });
1698
- }
1699
- });
1700
-
1701
- // DQM API Proxy - Start analysis
1702
- app.post('/dqm/assets', authenticateSession, async (req, res) => {
1703
- const { apiKey, websiteId } = req.session;
1704
-
1705
- try {
1706
- const response = await axios.post(
1707
- 'https://api.crownpeak.net/dqm-cms/v1/assets',
1708
- req.body,
1709
- {
1710
- headers: { 'x-api-key': apiKey },
1711
- params: { apiKey, websiteId },
1712
- }
1713
- );
1714
-
1715
- res.json(response.data);
1716
- } catch (error) {
1717
- res.status(500).json({ message: 'Analysis failed' });
1718
- }
1719
- });
1720
-
1721
- // Middleware: Verify session token
1722
- function authenticateSession(req, res, next) {
1723
- const token = req.headers.authorization?.replace('Bearer ', '');
1724
- const session = sessions.get(token);
1725
-
1726
- if (!session || session.expiresAt < Date.now()) {
1727
- return res.status(401).json({ message: 'Invalid or expired session' });
1728
- }
1729
-
1730
- req.session = session;
1731
- next();
1732
- }
1733
-
1734
- function generateUniqueToken() {
1735
- return require('crypto').randomBytes(32).toString('hex');
1736
- }
1737
-
1738
- app.listen(3000);
1739
- ```
1740
-
1741
- ---
1742
-
1743
- ## Frontend Configuration
1744
-
1745
- ```tsx
1746
- import { DQMSidebar } from '@crownpeak/dqm-react-component';
1747
-
1748
- function App() {
1749
- return (
1750
- <DQMSidebar
1751
- open={true}
1752
- onOpen={() => {}}
1753
- onClose={() => {}}
1754
- config={{
1755
- authBackendUrl: 'https://your-backend.com', // Backend proxy base URL
1756
- useLocalStorage: true, // Store session token
1757
-
1758
- // Optional: OAuth2 configuration
1759
- oauth2Config: {
1760
- authUrl: 'https://oauth-provider.com/authorize',
1761
- tokenUrl: 'https://oauth-provider.com/token',
1762
- clientId: 'your_client_id',
1763
- redirectUri: 'https://your-app.com/oauth-callback',
1764
- scope: 'dqm:read',
1765
- },
1766
- }}
1767
- onAuthSuccess={(creds) => {
1768
- console.log('Authenticated:', creds.sessionType); // 'backend'
1769
- }}
1770
- />
1771
- );
1772
- }
1773
- ```
1774
-
1775
- ---
1776
-
1777
- ## Testing
1778
-
1779
- ### Test Direct Credentials Flow
1780
-
1781
- ```bash
1782
- curl -X POST https://your-backend.com/auth/login \
1783
- -H "Content-Type: application/json" \
1784
- -d '{"apiKey":"test_key","websiteId":"test_id"}'
1785
- ```
1786
-
1787
- ### Test Analysis Proxy
1788
-
1789
- ```bash
1790
- curl -X POST https://your-backend.com/dqm/assets \
1791
- -H "Authorization: Bearer <session_token>" \
1792
- -H "Content-Type: application/json" \
1793
- -d '{"html":"<html>...</html>","url":"https://example.com"}'
1794
- ```
1795
-
1796
- ---
1797
-
1798
- ## Migration Guide
1799
-
1800
- ### From Direct Mode to Backend Mode
1801
-
1802
- 1. **Implement backend endpoints** as documented above
1803
- 2. **Update React component config:**
1804
- ```tsx
1805
- // Before (Direct Mode)
1806
- config={{
1807
- apiKey: 'user_api_key',
1808
- websiteId: 'user_website_id',
1809
- }}
1810
-
1811
- // After (Backend Mode)
1812
- config={{
1813
- authBackendUrl: 'https://your-backend.com',
1814
- }}
1815
- ```
1816
- 3. **Clear old localStorage:**
1817
- ```javascript
1818
- localStorage.removeItem('dqm_apiKey');
1819
- localStorage.removeItem('dqm_websiteID');
1820
- ```
1821
-
1822
- ---
1823
-
1824
- ## Support
1825
-
1826
- For questions or issues:
1827
- - Check Crownpeak DQM API docs: https://docs.crownpeak.com/dqm-api
1828
- - Review component source: `src/DQMSidebar.tsx`
1829
- - Check authentication flow: `src/components/auth/`