@crownpeak/dqm-react-component 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHENTICATION.md +281 -0
- package/BACKEND-API.md +1829 -0
- package/CHANGELOG.md +28 -0
- package/DEVELOPMENT.md +339 -0
- package/EXAMPLES.md +194 -0
- package/LICENSE +22 -0
- package/QUICKSTART.md +200 -0
- package/README.md +213 -0
- package/dist/DQMSidebar.d.ts +5 -0
- package/dist/DQMSidebar.d.ts.map +1 -0
- package/dist/ErrorBoundary.d.ts +34 -0
- package/dist/ErrorBoundary.d.ts.map +1 -0
- package/dist/auth-ui/assets/index-CehNKFGj.js +158 -0
- package/dist/auth-ui/index.html +30 -0
- package/dist/components/auth/DQMLogin.d.ts +16 -0
- package/dist/components/auth/DQMLogin.d.ts.map +1 -0
- package/dist/components/auth/OAuth2CallbackHandler.d.ts +15 -0
- package/dist/components/auth/OAuth2CallbackHandler.d.ts.map +1 -0
- package/dist/components/auth/index.d.ts +3 -0
- package/dist/components/auth/index.d.ts.map +1 -0
- package/dist/components/cards/CategoryCard.d.ts +2 -0
- package/dist/components/cards/CategoryCard.d.ts.map +1 -0
- package/dist/components/cards/FailedCheckpointsCard.d.ts +2 -0
- package/dist/components/cards/FailedCheckpointsCard.d.ts.map +1 -0
- package/dist/components/cards/QualityOverviewCard.d.ts +2 -0
- package/dist/components/cards/QualityOverviewCard.d.ts.map +1 -0
- package/dist/components/cards/index.d.ts +4 -0
- package/dist/components/cards/index.d.ts.map +1 -0
- package/dist/components/common/CircularProgressWithLabel.d.ts +5 -0
- package/dist/components/common/CircularProgressWithLabel.d.ts.map +1 -0
- package/dist/components/common/index.d.ts +2 -0
- package/dist/components/common/index.d.ts.map +1 -0
- package/dist/components/renderers/BrowserViewRenderer.d.ts +9 -0
- package/dist/components/renderers/BrowserViewRenderer.d.ts.map +1 -0
- package/dist/components/renderers/SafeParsedHtml.d.ts +4 -0
- package/dist/components/renderers/SafeParsedHtml.d.ts.map +1 -0
- package/dist/components/renderers/ShadowDOMRenderer.d.ts +11 -0
- package/dist/components/renderers/ShadowDOMRenderer.d.ts.map +1 -0
- package/dist/components/renderers/index.d.ts +4 -0
- package/dist/components/renderers/index.d.ts.map +1 -0
- package/dist/components/sidebar/SidebarContent.d.ts +2 -0
- package/dist/components/sidebar/SidebarContent.d.ts.map +1 -0
- package/dist/components/sidebar/SidebarFooter.d.ts +5 -0
- package/dist/components/sidebar/SidebarFooter.d.ts.map +1 -0
- package/dist/components/sidebar/SidebarHeader.d.ts +2 -0
- package/dist/components/sidebar/SidebarHeader.d.ts.map +1 -0
- package/dist/components/sidebar/SidebarSkeleton.d.ts +5 -0
- package/dist/components/sidebar/SidebarSkeleton.d.ts.map +1 -0
- package/dist/components/sidebar/StyledDrawer.d.ts +2 -0
- package/dist/components/sidebar/StyledDrawer.d.ts.map +1 -0
- package/dist/components/sidebar/StyledFab.d.ts +4 -0
- package/dist/components/sidebar/StyledFab.d.ts.map +1 -0
- package/dist/components/sidebar/index.d.ts +7 -0
- package/dist/components/sidebar/index.d.ts.map +1 -0
- package/dist/index.cjs +113 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13712 -0
- package/dist/index.js.map +1 -0
- package/dist/server/config.js +21 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/index.js +74 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/middleware/authenticate.js +31 -0
- package/dist/server/middleware/authenticate.js.map +1 -0
- package/dist/server/middleware/errorHandler.js +8 -0
- package/dist/server/middleware/errorHandler.js.map +1 -0
- package/dist/server/routes/auth.js +142 -0
- package/dist/server/routes/auth.js.map +1 -0
- package/dist/server/routes/dqm.js +138 -0
- package/dist/server/routes/dqm.js.map +1 -0
- package/dist/server/services/dqmClient.js +127 -0
- package/dist/server/services/dqmClient.js.map +1 -0
- package/dist/server/services/sessionStore.js +250 -0
- package/dist/server/services/sessionStore.js.map +1 -0
- package/dist/server/types.js +2 -0
- package/dist/server/types.js.map +1 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/colors/GenerateCategoryColors.d.ts +12 -0
- package/dist/utils/colors/GenerateCategoryColors.d.ts.map +1 -0
- package/dist/utils/localStorage.d.ts +4 -0
- package/dist/utils/localStorage.d.ts.map +1 -0
- package/dist/utils/storage.d.ts +28 -0
- package/dist/utils/storage.d.ts.map +1 -0
- package/package.json +124 -0
package/BACKEND-API.md
ADDED
|
@@ -0,0 +1,1829 @@
|
|
|
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/`
|