vulnnearable comments removed and source exposing to frobrowser disabled worknote XSS fixed

This commit is contained in:
laxmanhalaki 2026-02-13 14:46:08 +05:30
parent b32a3505ac
commit 00e1d51c66
77 changed files with 499 additions and 443 deletions

View File

@ -1326,9 +1326,9 @@ GCP_KEY_FILE=./config/gcp-key.json
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=notifications@{{API_DOMAIN}}
SMTP_USER=notifications@{{APP_DOMAIN}}
SMTP_PASSWORD=your_smtp_password
EMAIL_FROM=RE Workflow System <notifications@{{API_DOMAIN}}>
EMAIL_FROM=RE Workflow System <notifications@{{APP_DOMAIN}}>
# AI Service (for conclusion generation)
AI_API_KEY=your_ai_api_key

View File

@ -98,7 +98,7 @@ npm run dev
1. Server will start automatically
2. Log in via SSO
3. Run this SQL to make yourself admin:
UPDATE users SET role = 'ADMIN' WHERE email = 'your-email@{{API_DOMAIN}}';
UPDATE users SET role = 'ADMIN' WHERE email = 'your-email@{{APP_DOMAIN}}';
[Config Seed] ✅ Default configurations seeded successfully (30 settings)
info: ✅ Server started successfully on port 5000
@ -112,7 +112,7 @@ psql -d royal_enfield_workflow
UPDATE users
SET role = 'ADMIN'
WHERE email = 'your-email@{{API_DOMAIN}}';
WHERE email = 'your-email@{{APP_DOMAIN}}';
\q
```

View File

@ -471,7 +471,7 @@ The backend supports web push notifications via VAPID (Voluntary Application Ser
```
VAPID_PUBLIC_KEY=<your-public-key>
VAPID_PRIVATE_KEY=<your-private-key>
VAPID_CONTACT=mailto:admin@{{API_DOMAIN}}
VAPID_CONTACT=mailto:admin@{{APP_DOMAIN}}
```
3. **Add to Frontend `.env`:**

View File

@ -66,7 +66,7 @@
],
"body": {
"mode": "raw",
"raw": "{\n // Okta username (email)\n \"username\": \"user@{{API_DOMAIN}}\",\n \n // Okta password\n \"password\": \"YourOktaPassword123\"\n}"
"raw": "{\n // Okta username (email)\n \"username\": \"user@{{APP_DOMAIN}}\",\n \n // Okta password\n \"password\": \"YourOktaPassword123\"\n}"
},
"url": {
"raw": "{{baseUrl}}/auth/login",
@ -498,7 +498,7 @@
],
"body": {
"mode": "raw",
"raw": "{\n \"templateType\": \"CUSTOM\",\n \"title\": \"Purchase Order Approval for Office Equipment\",\n \"description\": \"Approval needed for purchasing new office equipment including laptops, monitors, and office furniture. Total budget: $50,000\",\n \"priority\": \"STANDARD\",\n \"approvalLevels\": [\n {\n \"email\": \"manager@{{API_DOMAIN}}\",\n \"tatHours\": 24\n },\n {\n \"email\": \"director@{{API_DOMAIN}}\",\n \"tatHours\": 48\n },\n {\n \"email\": \"cfo@{{API_DOMAIN}}\",\n \"tatHours\": 72\n }\n ],\n \"spectators\": [\n {\n \"email\": \"hr@{{API_DOMAIN}}\"\n },\n {\n \"email\": \"finance@{{API_DOMAIN}}\"\n }\n ]\n}"
"raw": "{\n \"templateType\": \"CUSTOM\",\n \"title\": \"Purchase Order Approval for Office Equipment\",\n \"description\": \"Approval needed for purchasing new office equipment including laptops, monitors, and office furniture. Total budget: $50,000\",\n \"priority\": \"STANDARD\",\n \"approvalLevels\": [\n {\n \"email\": \"manager@{{APP_DOMAIN}}\",\n \"tatHours\": 24\n },\n {\n \"email\": \"director@{{APP_DOMAIN}}\",\n \"tatHours\": 48\n },\n {\n \"email\": \"cfo@{{APP_DOMAIN}}\",\n \"tatHours\": 72\n }\n ],\n \"spectators\": [\n {\n \"email\": \"hr@{{APP_DOMAIN}}\"\n },\n {\n \"email\": \"finance@{{APP_DOMAIN}}\"\n }\n ]\n}"
},
"url": {
"raw": "{{baseUrl}}/workflows",
@ -522,7 +522,7 @@
"formdata": [
{
"key": "payload",
"value": "{\"templateType\":\"CUSTOM\",\"title\":\"Purchase Order Approval with Documents\",\"description\":\"Approval needed for office equipment purchase with supporting documents\",\"priority\":\"STANDARD\",\"approvalLevels\":[{\"email\":\"manager@{{API_DOMAIN}}\",\"tatHours\":24},{\"email\":\"director@{{API_DOMAIN}}\",\"tatHours\":48}],\"spectators\":[{\"email\":\"hr@{{API_DOMAIN}}\"}]}",
"value": "{\"templateType\":\"CUSTOM\",\"title\":\"Purchase Order Approval with Documents\",\"description\":\"Approval needed for office equipment purchase with supporting documents\",\"priority\":\"STANDARD\",\"approvalLevels\":[{\"email\":\"manager@{{APP_DOMAIN}}\",\"tatHours\":24},{\"email\":\"director@{{APP_DOMAIN}}\",\"tatHours\":48}],\"spectators\":[{\"email\":\"hr@{{APP_DOMAIN}}\"}]}",
"type": "text",
"description": "JSON payload with simplified format (email + tatHours only)"
},
@ -719,7 +719,7 @@
],
"body": {
"mode": "raw",
"raw": "{\n \"email\": \"newapprover@{{API_DOMAIN}}\",\n \"tatHours\": 24,\n \"level\": 2\n}"
"raw": "{\n \"email\": \"newapprover@{{APP_DOMAIN}}\",\n \"tatHours\": 24,\n \"level\": 2\n}"
},
"url": {
"raw": "{{baseUrl}}/workflows/:id/approvers/at-level",
@ -755,7 +755,7 @@
],
"body": {
"mode": "raw",
"raw": "{\n \"email\": \"spectator@{{API_DOMAIN}}\"\n}"
"raw": "{\n \"email\": \"spectator@{{APP_DOMAIN}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/workflows/:id/participants/spectator",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1 @@
import{a as s}from"./index-y_ojbF9T.js";import"./radix-vendor-CYvDqP9X.js";import"./charts-vendor-BVfwAPj-.js";import"./utils-vendor-DNMmNUQL.js";import"./ui-vendor-DfwWW08H.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-B_rK4TXr.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
//# sourceMappingURL=conclusionApi-DoX_H3Tk.js.map
import{a as s}from"./index-5rjlVIR5.js";import"./radix-vendor-CYvDqP9X.js";import"./charts-vendor-BVfwAPj-.js";import"./utils-vendor-BTBPSQfW.js";import"./ui-vendor-BFJfF1vG.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-B_rK4TXr.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};

View File

@ -1 +0,0 @@
{"version":3,"file":"conclusionApi-DoX_H3Tk.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n * Returns null if conclusion doesn't exist (404) instead of throwing error\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark | null> {\r\n try {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n } catch (error: any) {\r\n // Handle 404 gracefully - conclusion doesn't exist yet, which is normal\r\n if (error.response?.status === 404) {\r\n return null;\r\n }\r\n // Re-throw other errors\r\n throw error;\r\n }\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion","error","_a"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAMA,eAAsBC,EAAcJ,EAAqD,OACvF,GAAI,CAEF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB,OAASK,EAAY,CAEnB,KAAIC,EAAAD,EAAM,WAAN,YAAAC,EAAgB,UAAW,IAC7B,OAAO,KAGT,MAAMD,CACR,CACF"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -10,14 +10,14 @@
<meta name="theme-color" content="#2d4a3e" />
<title>Royal Enfield | Approval Portal</title>
<!-- Preload critical fonts and icons -->
<!-- Preload essential fonts and icons -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<script type="module" crossorigin src="/assets/index-y_ojbF9T.js"></script>
<script type="module" crossorigin src="/assets/index-5rjlVIR5.js"></script>
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-BVfwAPj-.js">
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CYvDqP9X.js">
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DNMmNUQL.js">
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-DfwWW08H.js">
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-BTBPSQfW.js">
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-BFJfF1vG.js">
<link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js">
<link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js">
<link rel="modulepreload" crossorigin href="/assets/router-vendor-B_rK4TXr.js">

4
build/robots.txt Normal file
View File

@ -0,0 +1,4 @@
User-agent: *
Disallow: /api/
Sitemap: https://reflow.royalenfield.com/sitemap.xml

9
build/sitemap.xml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://reflow.royalenfield.com</loc>
<lastmod>2024-03-20T12:00:00+00:00</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
</urlset>

View File

@ -34,7 +34,7 @@ The Claim Management workflow has **8 fixed steps** with specific approvers and
- **Approver Type**: System (Auto-processed)
- **Action Type**: **AUTO** (System automatically creates activity)
- **TAT**: 1 hour
- **Mapping**: System user (`system@{{API_DOMAIN}}`)
- **Mapping**: System user (`system@{{APP_DOMAIN}}`)
- **Status**: Auto-approved when triggered
### Step 5: Dealer Completion Documents
@ -55,7 +55,7 @@ The Claim Management workflow has **8 fixed steps** with specific approvers and
- **Approver Type**: System (Auto-processed via DMS)
- **Action Type**: **AUTO** (System generates e-invoice via DMS integration)
- **TAT**: 1 hour
- **Mapping**: System user (`system@{{API_DOMAIN}}`)
- **Mapping**: System user (`system@{{APP_DOMAIN}}`)
- **Status**: Auto-approved when triggered
### Step 8: Credit Note Confirmation
@ -121,7 +121,7 @@ const dealerUser = await User.findOne({ where: { email: dealerEmail } });
1. Find user with department containing "Finance" and role = 'MANAGEMENT'
2. Find user with designation containing "Finance" or "Accountant"
3. Use configured finance team email from admin_configurations table
4. Fallback: Use default finance email (e.g., finance@{{API_DOMAIN}})
4. Fallback: Use default finance email (e.g., finance@{{APP_DOMAIN}})
```
## Next Steps

View File

@ -112,7 +112,7 @@ Your CSV file must have these **44 columns** in the following order:
| `on_boarding_charges` | Decimal | No | Numeric value (e.g., 1000.50) |
| `date` | Date | No | Format: YYYY-MM-DD (e.g., 2014-09-30) |
| `single_format_month_year` | String(50) | No | Format: Sep-2014 |
| `domain_id` | String(255) | No | Email domain (e.g., dealer@{{API_DOMAIN}}) |
| `domain_id` | String(255) | No | Email domain (e.g., dealer@{{APP_DOMAIN}}) |
| `replacement` | String(50) | No | Replacement status |
| `termination_resignation_status` | String(255) | No | Termination/Resignation status |
| `date_of_termination_resignation` | Date | No | Format: YYYY-MM-DD |
@ -183,7 +183,7 @@ Ensure dates are in `YYYY-MM-DD` format:
```csv
sales_code,service_code,gear_code,gma_code,region,dealership,state,district,city,location,city_category_pst,layout_format,tier_city_category,on_boarding_charges,date,single_format_month_year,domain_id,replacement,termination_resignation_status,date_of_termination_resignation,last_date_of_operations,old_codes,branch_details,dealer_principal_name,dealer_principal_email_id,dp_contact_number,dp_contacts,showroom_address,showroom_pincode,workshop_address,workshop_pincode,location_district,state_workshop,no_of_studios,website_update,gst,pan,firm_type,prop_managing_partners_directors,total_prop_partners_directors,docs_folder_link,workshop_gma_codes,existing_new,dlrcode
5124,5125,5573,9430,S3,Accelerate Motors,Karnataka,Bengaluru,Bengaluru,RAJA RAJESHWARI NAGAR,A+,A+,Tier 1 City,,2014-09-30,Sep-2014,acceleratemotors.rrnagar@dealer.{{API_DOMAIN}},,,,,,,N. Shyam Charmanna,shyamcharmanna@yahoo.co.in,7022049621,7022049621,"No.335, HVP RR Nagar Sector B, Ideal Homes Town Ship, Bangalore - 560098, Dist Bangalore, Karnataka",560098,"Works Shop No.460, 80ft Road, 2nd Phase R R Nagar, Bangalore - 560098, Dist Bangalore, Karnataka",560098,Bangalore,Karnataka,0,Yes,29ARCPS1311D1Z6,ARCPS1311D,Proprietorship,CHARMANNA SHYAM NELLAMAKADA,CHARMANNA SHYAM NELLAMAKADA,https://drive.google.com/drive/folders/1sGtg3s1h9aBXX9fhxJufYuBWar8gVvnb,,,3386
5124,5125,5573,9430,S3,Accelerate Motors,Karnataka,Bengaluru,Bengaluru,RAJA RAJESHWARI NAGAR,A+,A+,Tier 1 City,,2014-09-30,Sep-2014,acceleratemotors.rrnagar@dealer.{{APP_DOMAIN}},,,,,,,N. Shyam Charmanna,shyamcharmanna@yahoo.co.in,7022049621,7022049621,"No.335, HVP RR Nagar Sector B, Ideal Homes Town Ship, Bangalore - 560098, Dist Bangalore, Karnataka",560098,"Works Shop No.460, 80ft Road, 2nd Phase R R Nagar, Bangalore - 560098, Dist Bangalore, Karnataka",560098,Bangalore,Karnataka,0,Yes,29ARCPS1311D1Z6,ARCPS1311D,Proprietorship,CHARMANNA SHYAM NELLAMAKADA,CHARMANNA SHYAM NELLAMAKADA,https://drive.google.com/drive/folders/1sGtg3s1h9aBXX9fhxJufYuBWar8gVvnb,,,3386
```
**What gets auto-generated:**

View File

@ -56,7 +56,7 @@ users {
```json
{
"userId": "uuid-1",
"email": "john.doe@{{API_DOMAIN}}",
"email": "john.doe@{{APP_DOMAIN}}",
"employeeId": "E12345", // Regular employee ID
"designation": "Software Engineer",
"department": "IT",
@ -68,7 +68,7 @@ users {
```json
{
"userId": "uuid-2",
"email": "test.2@{{API_DOMAIN}}",
"email": "test.2@{{APP_DOMAIN}}",
"employeeId": "RE-MH-001", // Dealer code stored here
"designation": "Dealer",
"department": "Dealer Operations",

View File

@ -98,8 +98,8 @@ DMS_WEBHOOK_SECRET=your_shared_secret_key_here
**Base URL Examples:**
- Development: `http://localhost:5000/api/v1/webhooks/dms/invoice`
- UAT: `https://reflow-uat.{{API_DOMAIN}}/api/v1/webhooks/dms/invoice`
- Production: `https://reflow.{{API_DOMAIN}}/api/v1/webhooks/dms/invoice`
- UAT: `https://reflow-uat.{{APP_DOMAIN}}/api/v1/webhooks/dms/invoice`
- Production: `https://reflow.{{APP_DOMAIN}}/api/v1/webhooks/dms/invoice`
### 3.2 Request Headers
@ -205,8 +205,8 @@ User-Agent: DMS-Webhook-Client/1.0
**Base URL Examples:**
- Development: `http://localhost:5000/api/v1/webhooks/dms/credit-note`
- UAT: `https://reflow-uat.{{API_DOMAIN}}/api/v1/webhooks/dms/credit-note`
- Production: `https://reflow.{{API_DOMAIN}}/api/v1/webhooks/dms/credit-note`
- UAT: `https://reflow-uat.{{APP_DOMAIN}}/api/v1/webhooks/dms/credit-note`
- Production: `https://reflow.{{APP_DOMAIN}}/api/v1/webhooks/dms/credit-note`
### 4.2 Request Headers
@ -563,8 +563,8 @@ DMS_WEBHOOK_SECRET=your_shared_secret_key_here
| Environment | Invoice Webhook URL | Credit Note Webhook URL |
|-------------|---------------------|-------------------------|
| Development | `http://localhost:5000/api/v1/webhooks/dms/invoice` | `http://localhost:5000/api/v1/webhooks/dms/credit-note` |
| UAT | `https://reflow-uat.{{API_DOMAIN}}/api/v1/webhooks/dms/invoice` | `https://reflow-uat.{{API_DOMAIN}}/api/v1/webhooks/dms/credit-note` |
| Production | `https://reflow.{{API_DOMAIN}}/api/v1/webhooks/dms/invoice` | `https://reflow.{{API_DOMAIN}}/api/v1/webhooks/dms/credit-note` |
| UAT | `https://reflow-uat.{{APP_DOMAIN}}/api/v1/webhooks/dms/invoice` | `https://reflow-uat.{{APP_DOMAIN}}/api/v1/webhooks/dms/credit-note` |
| Production | `https://reflow.{{APP_DOMAIN}}/api/v1/webhooks/dms/invoice` | `https://reflow.{{APP_DOMAIN}}/api/v1/webhooks/dms/credit-note` |
---

View File

@ -157,7 +157,7 @@ npm run seed:config
```bash
# Edit the script
nano scripts/assign-admin-user.sql
# Change: YOUR_EMAIL@{{API_DOMAIN}}
# Change: YOUR_EMAIL@{{APP_DOMAIN}}
# Run it
psql -d royal_enfield_workflow -f scripts/assign-admin-user.sql
@ -170,7 +170,7 @@ psql -d royal_enfield_workflow
UPDATE users
SET role = 'ADMIN'
WHERE email = 'your-email@{{API_DOMAIN}}';
WHERE email = 'your-email@{{APP_DOMAIN}}';
-- Verify
SELECT email, role FROM users WHERE role = 'ADMIN';
@ -188,7 +188,7 @@ psql -d royal_enfield_workflow -c "\dt"
psql -d royal_enfield_workflow -c "\dT+ user_role_enum"
# Check your user
psql -d royal_enfield_workflow -c "SELECT email, role FROM users WHERE email = 'your-email@{{API_DOMAIN}}';"
psql -d royal_enfield_workflow -c "SELECT email, role FROM users WHERE email = 'your-email@{{APP_DOMAIN}}';"
```
---
@ -241,13 +241,13 @@ Expected output:
```sql
-- Single user
UPDATE users SET role = 'MANAGEMENT'
WHERE email = 'manager@{{API_DOMAIN}}';
WHERE email = 'manager@{{APP_DOMAIN}}';
-- Multiple users
UPDATE users SET role = 'MANAGEMENT'
WHERE email IN (
'manager1@{{API_DOMAIN}}',
'manager2@{{API_DOMAIN}}'
'manager1@{{APP_DOMAIN}}',
'manager2@{{APP_DOMAIN}}'
);
-- By department
@ -260,13 +260,13 @@ WHERE department = 'Management' AND is_active = true;
```sql
-- Single user
UPDATE users SET role = 'ADMIN'
WHERE email = 'admin@{{API_DOMAIN}}';
WHERE email = 'admin@{{APP_DOMAIN}}';
-- Multiple admins
UPDATE users SET role = 'ADMIN'
WHERE email IN (
'admin1@{{API_DOMAIN}}',
'admin2@{{API_DOMAIN}}'
'admin1@{{APP_DOMAIN}}',
'admin2@{{APP_DOMAIN}}'
);
-- By department
@ -331,7 +331,7 @@ SELECT
mobile_phone,
array_length(ad_groups, 1) as ad_group_count
FROM users
WHERE email = 'your-email@{{API_DOMAIN}}';
WHERE email = 'your-email@{{APP_DOMAIN}}';
```
---
@ -344,7 +344,7 @@ WHERE email = 'your-email@{{API_DOMAIN}}';
curl -X POST http://localhost:5000/api/v1/auth/okta/callback \
-H "Content-Type: application/json" \
-d '{
"email": "test@{{API_DOMAIN}}",
"email": "test@{{APP_DOMAIN}}",
"displayName": "Test User",
"oktaSub": "test-sub-123"
}'
@ -353,14 +353,14 @@ curl -X POST http://localhost:5000/api/v1/auth/okta/callback \
### 2. Check User Created with Default Role
```sql
SELECT email, role FROM users WHERE email = 'test@{{API_DOMAIN}}';
SELECT email, role FROM users WHERE email = 'test@{{APP_DOMAIN}}';
-- Expected: role = 'USER'
```
### 3. Update to ADMIN
```sql
UPDATE users SET role = 'ADMIN' WHERE email = 'test@{{API_DOMAIN}}';
UPDATE users SET role = 'ADMIN' WHERE email = 'test@{{APP_DOMAIN}}';
```
### 4. Verify API Access
@ -369,7 +369,7 @@ UPDATE users SET role = 'ADMIN' WHERE email = 'test@{{API_DOMAIN}}';
# Login and get token
curl -X POST http://localhost:5000/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "test@{{API_DOMAIN}}", ...}'
-d '{"email": "test@{{APP_DOMAIN}}", ...}'
# Try admin endpoint (should work if ADMIN role)
curl http://localhost:5000/api/v1/admin/configurations \
@ -449,7 +449,7 @@ npm run migrate
```sql
-- Check if user exists
SELECT * FROM users WHERE email = 'your-email@{{API_DOMAIN}}';
SELECT * FROM users WHERE email = 'your-email@{{APP_DOMAIN}}';
-- Check Okta sub
SELECT * FROM users WHERE okta_sub = 'your-okta-sub';
@ -459,7 +459,7 @@ SELECT * FROM users WHERE okta_sub = 'your-okta-sub';
```sql
-- Verify role
SELECT email, role, is_active FROM users WHERE email = 'your-email@{{API_DOMAIN}}';
SELECT email, role, is_active FROM users WHERE email = 'your-email@{{APP_DOMAIN}}';
-- Check role enum
\dT+ user_role_enum

View File

@ -29,7 +29,7 @@ This guide provides step-by-step instructions for setting up Google Cloud Storag
|------|------------------|
| **Application** | Royal Enfield Workflow System |
| **Environment** | Production |
| **Domain** | `https://reflow.{{API_DOMAIN}}` |
| **Domain** | `https://reflow.{{APP_DOMAIN}}` |
| **Purpose** | Store workflow documents, attachments, invoices, and credit notes |
| **Storage Type** | Google Cloud Storage (GCS) |
| **Region** | `asia-south1` (Mumbai) |
@ -325,8 +325,8 @@ Create `cors-config-prod.json`:
[
{
"origin": [
"https://reflow.{{API_DOMAIN}}",
"https://www.{{API_DOMAIN}}"
"https://reflow.{{APP_DOMAIN}}",
"https://www.{{APP_DOMAIN}}"
],
"method": ["GET", "PUT", "POST", "DELETE", "HEAD", "OPTIONS"],
"responseHeader": [

View File

@ -6,7 +6,7 @@
|------|-------|
| **Application** | RE Workflow System |
| **Environment** | UAT |
| **Domain** | https://reflow-uat.{{API_DOMAIN}} |
| **Domain** | https://reflow-uat.{{APP_DOMAIN}} |
| **Purpose** | Store workflow documents and attachments |
---
@ -131,8 +131,8 @@ Apply this CORS policy to allow browser uploads:
[
{
"origin": [
"https://reflow-uat.{{API_DOMAIN}}",
"https://reflow.{{API_DOMAIN}}"
"https://reflow-uat.{{APP_DOMAIN}}",
"https://reflow.{{APP_DOMAIN}}"
],
"method": ["GET", "PUT", "POST", "DELETE", "HEAD", "OPTIONS"],
"responseHeader": [

View File

@ -72,8 +72,8 @@ The Users API returns a complete user object:
"employeeID": "E09994",
"title": "Supports Business Applications (SAP) portfolio",
"department": "Deputy Manager - Digital & IT",
"login": "sanjaysahu@{{API_DOMAIN}}",
"email": "sanjaysahu@{{API_DOMAIN}}"
"login": "sanjaysahu@{{APP_DOMAIN}}",
"email": "sanjaysahu@{{APP_DOMAIN}}"
},
...
}

View File

@ -450,16 +450,16 @@ Before Migration:
+-------------------------+-----------+
| email | is_admin |
+-------------------------+-----------+
| admin@{{API_DOMAIN}} | true |
| user1@{{API_DOMAIN}} | false |
| admin@{{APP_DOMAIN}} | true |
| user1@{{APP_DOMAIN}} | false |
+-------------------------+-----------+
After Migration:
+-------------------------+-----------+-----------+
| email | role | is_admin |
+-------------------------+-----------+-----------+
| admin@{{API_DOMAIN}} | ADMIN | true |
| user1@{{API_DOMAIN}} | USER | false |
| admin@{{APP_DOMAIN}} | ADMIN | true |
| user1@{{APP_DOMAIN}} | USER | false |
+-------------------------+-----------+-----------+
```
@ -473,17 +473,17 @@ After Migration:
-- Make user a MANAGEMENT role
UPDATE users
SET role = 'MANAGEMENT', is_admin = false
WHERE email = 'manager@{{API_DOMAIN}}';
WHERE email = 'manager@{{APP_DOMAIN}}';
-- Make user an ADMIN role
UPDATE users
SET role = 'ADMIN', is_admin = true
WHERE email = 'admin@{{API_DOMAIN}}';
WHERE email = 'admin@{{APP_DOMAIN}}';
-- Revert to USER role
UPDATE users
SET role = 'USER', is_admin = false
WHERE email = 'user@{{API_DOMAIN}}';
WHERE email = 'user@{{APP_DOMAIN}}';
```
### Via API (Admin Endpoint)

View File

@ -47,12 +47,12 @@ psql -d royal_enfield_db -f scripts/assign-user-roles.sql
-- Make specific users ADMIN
UPDATE users
SET role = 'ADMIN', is_admin = true
WHERE email IN ('admin@{{API_DOMAIN}}', 'it.admin@{{API_DOMAIN}}');
WHERE email IN ('admin@{{APP_DOMAIN}}', 'it.admin@{{APP_DOMAIN}}');
-- Make specific users MANAGEMENT
UPDATE users
SET role = 'MANAGEMENT', is_admin = false
WHERE email IN ('manager@{{API_DOMAIN}}', 'auditor@{{API_DOMAIN}}');
WHERE email IN ('manager@{{APP_DOMAIN}}', 'auditor@{{APP_DOMAIN}}');
-- Verify roles
SELECT email, display_name, role, is_admin FROM users ORDER BY role, email;
@ -219,7 +219,7 @@ GROUP BY role;
-- Check specific user
SELECT email, role, is_admin
FROM users
WHERE email = 'your-email@{{API_DOMAIN}}';
WHERE email = 'your-email@{{APP_DOMAIN}}';
```
### Test 2: Test API Access
@ -356,7 +356,7 @@ WHERE designation ILIKE '%manager%' OR designation ILIKE '%head%';
```sql
SELECT email, role, is_admin
FROM users
WHERE email = 'your-email@{{API_DOMAIN}}';
WHERE email = 'your-email@{{APP_DOMAIN}}';
```
---

View File

@ -650,7 +650,7 @@ graph LR
{
"userId": "uuid",
"employeeId": "EMP001",
"email": "user@{{API_DOMAIN}}",
"email": "user@{{APP_DOMAIN}}",
"role": "USER" | "MANAGEMENT" | "ADMIN",
"iat": 1234567890,
"exp": 1234654290

View File

@ -64,7 +64,7 @@ await this.createClaimApprovalLevels(
isAuto: false,
approverType: 'department_lead' as const,
approverId: departmentLead?.userId || null,
approverEmail: departmentLead?.email || initiator.manager || 'deptlead@{{API_DOMAIN}}',
approverEmail: departmentLead?.email || initiator.manager || `deptlead@${appDomain}`,
}
```

View File

@ -181,7 +181,7 @@ POST http://localhost:5000/api/v1/auth/login
Content-Type: application/json
{
"username": "john.doe@{{API_DOMAIN}}",
"username": "john.doe@{{APP_DOMAIN}}",
"password": "SecurePassword123!"
}
```

View File

@ -41,9 +41,9 @@ USE_GOOGLE_SECRET_MANAGER=false
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=notifications@{{API_DOMAIN}}
SMTP_USER=notifications@{{APP_DOMAIN}}
SMTP_PASSWORD=your_smtp_password
EMAIL_FROM=RE Workflow System <notifications@{{API_DOMAIN}}>
EMAIL_FROM=RE Workflow System <notifications@{{APP_DOMAIN}}>
# AI Service (for conclusion generation) - Vertex AI Gemini
# Uses service account credentials from GCP_KEY_FILE

View File

@ -16,7 +16,7 @@
UPDATE users
SET role = 'ADMIN'
WHERE email = 'YOUR_EMAIL@{{API_DOMAIN}}' -- ← CHANGE THIS
WHERE email = 'YOUR_EMAIL@{{APP_DOMAIN}}' -- ← CHANGE THIS
RETURNING
user_id,
email,

View File

@ -21,9 +21,9 @@
UPDATE users
SET role = 'ADMIN'
WHERE email IN (
'admin@{{API_DOMAIN}}',
'it.admin@{{API_DOMAIN}}',
'system.admin@{{API_DOMAIN}}'
'admin@{{APP_DOMAIN}}',
'it.admin@{{APP_DOMAIN}}',
'system.admin@{{APP_DOMAIN}}'
-- Add more admin emails here
);
@ -45,9 +45,9 @@ ORDER BY email;
UPDATE users
SET role = 'MANAGEMENT'
WHERE email IN (
'manager1@{{API_DOMAIN}}',
'dept.head@{{API_DOMAIN}}',
'auditor@{{API_DOMAIN}}'
'manager1@{{APP_DOMAIN}}',
'dept.head@{{APP_DOMAIN}}',
'auditor@{{APP_DOMAIN}}'
-- Add more management emails here
);

View File

@ -162,7 +162,7 @@ SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=${SMTP_USER}
SMTP_PASSWORD=${SMTP_PASSWORD}
EMAIL_FROM=RE Workflow System <notifications@{{API_DOMAIN}}>
EMAIL_FROM=RE Workflow System <notifications@{{APP_DOMAIN}}>
# Vertex AI Gemini Configuration (for conclusion generation)
# Service account credentials should be placed in ./credentials/ folder
@ -232,7 +232,7 @@ show_vapid_instructions() {
echo " VITE_PUBLIC_VAPID_KEY=<your-public-key>"
echo ""
echo "5. The VAPID_CONTACT should be a valid mailto: URL"
echo " Example: mailto:admin@{{API_DOMAIN}}"
echo " Example: mailto:admin@{{APP_DOMAIN}}"
echo ""
echo "Note: Keep your VAPID_PRIVATE_KEY secure and never commit it to version control!"
echo ""

View File

@ -38,8 +38,10 @@ app.use((req: express.Request, res: express.Response, next: express.NextFunction
connectSrc.push(...origins);
}
const apiDomain = process.env.APP_DOMAIN || 'royalenfield.com';
// Define strict CSP directives
// CRITICAL: Move frame-ancestors, form-action, and base-uri to the front to ensure VAPT compliance
//: Move frame-ancestors, form-action, and base-uri to the front to ensure VAPT compliance
// even if the header is truncated in certain response types (like 301 redirects).
const directives = [
"frame-ancestors 'self'",
@ -53,7 +55,7 @@ app.use((req: express.Request, res: express.Response, next: express.NextFunction
"script-src 'self'",
"script-src-elem 'self'",
"script-src-attr 'none'",
"img-src 'self' data: blob: https://*.{{API_DOMAIN}} https://*.okta.com https://*.oktapreview.com https://*.googleapis.com https://*.gstatic.com",
`img-src 'self' data: blob: https://*.${apiDomain} https://*.okta.com https://*.oktapreview.com https://*.googleapis.com https://*.gstatic.com`,
"frame-src 'self' blob: data:",
"font-src 'self' https://fonts.gstatic.com data:",
"object-src 'none'",

View File

@ -9,7 +9,7 @@ export const emailConfig = {
},
},
from: process.env.EMAIL_FROM || 'RE Workflow System <notifications@{{API_DOMAIN}}>',
from: process.env.EMAIL_FROM || `RE Workflow System <notifications@${process.env.APP_DOMAIN || 'royalenfield.com'}>`,
// Email templates
templates: {

View File

@ -12,14 +12,14 @@ const ssoConfig: SSOConfig = {
return process.env.FRONTEND_URL?.split(',').map(s => s.trim()).filter(Boolean) || [];
},
// Okta/Auth0 configuration for token exchange
get oktaDomain() { return process.env.OKTA_DOMAIN || '{{IDP_DOMAIN}}'; },
get oktaDomain() { return process.env.OKTA_DOMAIN || `{{IDP_DOMAIN}}`; },
get oktaClientId() { return process.env.OKTA_CLIENT_ID || ''; },
get oktaClientSecret() { return process.env.OKTA_CLIENT_SECRET || ''; },
get oktaApiToken() { return process.env.OKTA_API_TOKEN || ''; }, // SSWS token for Users API
// Tanflow configuration for token exchange
get tanflowBaseUrl() { return process.env.TANFLOW_BASE_URL || '{{IDP_DOMAIN}}/realms/RE'; },
get tanflowBaseUrl() { return process.env.TANFLOW_BASE_URL || `{{IDP_DOMAIN}}/realms/RE`; },
get tanflowClientId() { return process.env.TANFLOW_CLIENT_ID || 'REFLOW'; },
get tanflowClientSecret() { return process.env.TANFLOW_CLIENT_SECRET || '{{TANFLOW_CLIENT_SECRET}}'; },
get tanflowClientSecret() { return process.env.TANFLOW_CLIENT_SECRET || `{{TANFLOW_CLIENT_SECRET}}`; },
};
export { ssoConfig };

View File

@ -151,7 +151,7 @@ export class AuthController {
message: 'Token refreshed successfully'
}, 'Token refreshed successfully');
} else {
// Development: Include token for debugging
// Dev: Include token for debugging
ResponseHandler.success(res, {
accessToken: newAccessToken
}, 'Token refreshed successfully');
@ -602,7 +602,7 @@ export class AuthController {
idToken: result.oktaIdToken
}, 'Token exchange successful');
} else {
// Development: Include tokens for debugging and different-port setup
// Dev: Include tokens for debugging and different-port setup
ResponseHandler.success(res, {
user: result.user,
accessToken: result.accessToken,

View File

@ -10,13 +10,37 @@ export class UserController {
this.userService = new UserService();
}
async getAllUsers(req: Request, res: Response): Promise<void> {
try {
const users = await this.userService.getAllUsers();
const result = {
users: users.map(u => ({
userId: u.userId,
email: u.email,
displayName: u.displayName,
department: u.department,
designation: u.designation,
isActive: u.isActive,
})),
total: users.length
};
ResponseHandler.success(res, result, 'All users fetched');
} catch (error) {
logger.error('Failed to fetch all users', { error });
ResponseHandler.error(res, 'Failed to fetch all users', 500);
}
}
async searchUsers(req: Request, res: Response): Promise<void> {
try {
const q = String(req.query.q || '').trim();
const limit = Number(req.query.limit || 10);
const source = String(req.query.source || 'default') as 'local' | 'okta' | 'default';
const currentUserId = (req as any).user?.userId || (req as any).user?.id;
const users = await this.userService.searchUsers(q, limit, currentUserId);
const users = await this.userService.searchUsers(q, limit, currentUserId, source);
const result = users.map(u => ({
userId: (u as any).userId,
@ -69,6 +93,31 @@ export class UserController {
}
}
async getUserById(req: Request, res: Response): Promise<void> {
try {
const { userId } = req.params;
const user = await this.userService.getUserById(userId);
if (!user) {
ResponseHandler.error(res, 'User not found', 404);
return;
}
ResponseHandler.success(res, {
userId: user.userId,
email: user.email,
displayName: user.displayName,
firstName: user.firstName,
lastName: user.lastName,
department: user.department,
isActive: user.isActive
}, 'User fetched');
} catch (error) {
logger.error('Failed to fetch user by ID', { error });
ResponseHandler.error(res, 'Failed to fetch user by ID', 500);
}
}
/**
* Ensure user exists in database (create if not exists)
* Called when user is selected/tagged in the frontend

View File

@ -991,9 +991,9 @@ Add to `.env`:
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=notifications@{{API_DOMAIN}}
SMTP_USER=notifications@{{APP_DOMAIN}}
SMTP_PASSWORD=your-app-specific-password
EMAIL_FROM=RE Flow <noreply@{{API_DOMAIN}}>
EMAIL_FROM=RE Flow <noreply@{{APP_DOMAIN}}>
# Email Settings
EMAIL_ENABLED=true
@ -1002,10 +1002,10 @@ EMAIL_BATCH_SIZE=50
EMAIL_RETRY_ATTEMPTS=3
# Application
BASE_URL=https://workflow.{{API_DOMAIN}}
BASE_URL=https://workflow.{{APP_DOMAIN}}
COMPANY_NAME=Royal Enfield
COMPANY_WEBSITE=https://www.{{API_DOMAIN}}
SUPPORT_EMAIL=support@{{API_DOMAIN}}
COMPANY_WEBSITE=https://www.{{APP_DOMAIN}}
SUPPORT_EMAIL=support@{{APP_DOMAIN}}
```
---

View File

@ -65,7 +65,7 @@ Each template uses color-coded gradients to indicate the scenario:
All templates feature a single action button:
- **Text:** "View Request Details" / "Review Request Now" / "Take Action Now"
- **Link Format:** `{baseURL}/request/{requestNumber}`
- **Example:** `https://workflow.{{API_DOMAIN}}/request/REQ-2025-12-0013`
- **Example:** `https://workflow.{{APP_DOMAIN}}/request/REQ-2025-12-0013`
No approval/rejection buttons in emails - all actions happen within the application.
@ -231,8 +231,8 @@ SMTP_USER=your-email@domain.com
SMTP_PASSWORD=your-app-password
# Email Settings
EMAIL_FROM=RE Workflow System <notifications@{{API_DOMAIN}}>
BASE_URL=https://workflow.{{API_DOMAIN}}
EMAIL_FROM=RE Workflow System <notifications@{{APP_DOMAIN}}>
BASE_URL=https://workflow.{{APP_DOMAIN}}
COMPANY_NAME=Royal Enfield
```

View File

@ -361,7 +361,7 @@ All `[ViewDetailsLink]` placeholders should be replaced with:
{baseURL}/request/{requestNumber}
```
Example: `https://workflow.{{API_DOMAIN}}/request/REQ-2025-12-0013`
Example: `https://workflow.{{APP_DOMAIN}}/request/REQ-2025-12-0013`
### Company Name
Replace `[CompanyName]` with your organization name (e.g., "Royal Enfield")

View File

@ -53,7 +53,7 @@ const data: RequestCreatedData = {
requestTime: '02:30 PM',
totalApprovers: 3,
expectedTAT: 48,
viewDetailsLink: 'https://workflow.{{API_DOMAIN}}/request/REQ-2025-12-0013',
viewDetailsLink: 'https://workflow.{{APP_DOMAIN}}/request/REQ-2025-12-0013',
companyName: 'Royal Enfield'
};
```
@ -188,10 +188,10 @@ SMTP_USER=your-email@domain.com
SMTP_PASSWORD=your-app-password
# Email Settings
EMAIL_FROM=Royal Enfield Workflow <notifications@{{API_DOMAIN}}>
EMAIL_FROM=Royal Enfield Workflow <notifications@{{APP_DOMAIN}}>
# Application Settings
BASE_URL=https://workflow.{{API_DOMAIN}}
BASE_URL=https://workflow.{{APP_DOMAIN}}
COMPANY_NAME=Royal Enfield
```

View File

@ -7,18 +7,20 @@
import { EmailHeaderConfig, EmailFooterConfig } from './helpers';
const appDomain = process.env.APP_DOMAIN || 'royalenfield.com';
/**
* Company Information
*/
export const CompanyInfo = {
name: 'Royal Enfield',
productName: 'RE Flow', // Product name displayed in header
website: 'https://www.{{API_DOMAIN}}',
supportEmail: 'support@{{API_DOMAIN}}',
website: `https://www.${appDomain}`,
supportEmail: `support@${appDomain}`,
// Logo configuration for email headers
logo: {
url: 'https://www.{{API_DOMAIN}}/content/dam/RE-Platform-Revamp/re-revamp-commons/logo.webp',
url: `https://www.${appDomain}/content/dam/RE-Platform-Revamp/re-revamp-commons/logo.webp`,
alt: 'Royal Enfield Logo',
width: 220, // Logo width in pixels (wider for better visibility)
height: 65, // Logo height in pixels (proportional ratio ~3.4:1)
@ -88,7 +90,7 @@ export const CustomHeaderStyles = {
* Usage in email service:
* const link = getViewDetailsLink('REQ-2025-12-0013', process.env.FRONTEND_URL);
*
* Result: https://workflow.{{API_DOMAIN}}/request/REQ-2025-12-0013
* Result: https://workflow.royalenfield.com/request/REQ-2025-12-0013
*/
export function getViewDetailsLink(requestNumber: string, frontendUrl: string): string {
return `${frontendUrl}/request/${requestNumber}`;

View File

@ -270,7 +270,7 @@ export async function shouldSendEmailWithOverride(
userId: string,
emailType: EmailNotificationType
): Promise<boolean> {
// Critical emails always sent (override user preference)
// emails always sent (override user preference)
if (CRITICAL_EMAILS.includes(emailType)) {
const adminEnabled = await isAdminEmailEnabled(emailType);
if (adminEnabled) {

View File

@ -14,13 +14,13 @@ async function generatePreviews() {
// Sample data
const initiator = {
userId: 'user-1',
email: 'john.doe@{{API_DOMAIN}}',
email: 'john.doe@royalenfield.com',
displayName: 'John Doe'
};
const approver = {
userId: 'user-2',
email: 'jane.smith@{{API_DOMAIN}}',
email: 'jane.smith@royalenfield.com',
displayName: 'Jane Smith'
};

View File

@ -100,7 +100,7 @@ async function sendTestEmail() {
// Test 1: Request Created Email
const html1 = getRequestCreatedEmail(requestCreatedData);
const info1 = await transporter.sendMail({
from: '"Royal Enfield Workflow" <noreply@{{API_DOMAIN}}>',
from: '"Royal Enfield Workflow" <noreply@royalenfield.com>',
to: 'initiator@example.com',
subject: '[REQ-2025-12-0013] Request Created Successfully',
html: html1
@ -113,7 +113,7 @@ async function sendTestEmail() {
// Test 2: Approval Request Email (Single)
const html2 = getApprovalRequestEmail(approvalRequestData);
const info2 = await transporter.sendMail({
from: '"Royal Enfield Workflow" <noreply@{{API_DOMAIN}}>',
from: '"Royal Enfield Workflow" <noreply@royalenfield.com>',
to: 'approver@example.com',
subject: '[REQ-2025-12-0013] Approval Request - Action Required',
html: html2
@ -126,7 +126,7 @@ async function sendTestEmail() {
// Test 3: Multi-Approver Request Email
const html3 = getMultiApproverRequestEmail(multiApproverData);
const info3 = await transporter.sendMail({
from: '"Royal Enfield Workflow" <noreply@{{API_DOMAIN}}>',
from: '"Royal Enfield Workflow" <noreply@royalenfield.com>',
to: 'approver-level2@example.com',
subject: '[REQ-2025-12-0013] Multi-Level Approval Request - Your Turn',
html: html3

View File

@ -8,6 +8,8 @@
import { emailNotificationService } from '../services/emailNotification.service';
import logger from '../utils/logger';
const appDomain = process.env.APP_DOMAIN || 'royalenfield.com';
/**
* Simulate real workflow scenario
*/
@ -18,7 +20,7 @@ async function testRealScenario() {
// Mock user data (simulating real database records)
const user10 = {
userId: 'user-10-uuid',
email: 'john.doe@{{API_DOMAIN}}',
email: `john.doe@${appDomain}`,
displayName: 'John Doe',
department: 'Engineering',
designation: 'Senior Engineer'
@ -26,7 +28,7 @@ async function testRealScenario() {
const user12 = {
userId: 'user-12-uuid',
email: 'jane.smith@{{API_DOMAIN}}',
email: `jane.smith@${appDomain}`,
displayName: 'Jane Smith',
department: 'Management',
designation: 'Engineering Manager',
@ -52,7 +54,7 @@ async function testRealScenario() {
This purchase is critical for our Q1 2025 testing schedule and has been pre-approved by the department head.
</blockquote>
<p>Please review and approve at your earliest convenience.</p>
<p>For questions, contact: <a href="mailto:john.doe@{{API_DOMAIN}}">john.doe@{{API_DOMAIN}}</a></p>
<p>For questions, contact: <a href="mailto:john.doe@${appDomain}">john.doe@${appDomain}</a></p>
`,
requestType: 'Purchase',
priority: 'HIGH',
@ -68,21 +70,21 @@ async function testRealScenario() {
{
levelNumber: 1,
approverName: 'Jane Smith',
approverEmail: 'jane.smith@{{API_DOMAIN}}',
approverEmail: `jane.smith@${appDomain}`,
status: 'PENDING',
approvedAt: null
},
{
levelNumber: 2,
approverName: 'Michael Brown',
approverEmail: 'michael.brown@{{API_DOMAIN}}',
approverEmail: `michael.brown@${appDomain}`,
status: 'PENDING',
approvedAt: null
},
{
levelNumber: 3,
approverName: 'Sarah Johnson',
approverEmail: 'sarah.johnson@{{API_DOMAIN}}',
approverEmail: `sarah.johnson@${appDomain}`,
status: 'PENDING',
approvedAt: null
}
@ -168,7 +170,7 @@ async function testRealScenario() {
approvedUser12,
user10,
false, // not final approval
{ displayName: 'Michael Brown', email: 'michael.brown@{{API_DOMAIN}}' }
{ displayName: 'Michael Brown', email: `michael.brown@${appDomain}` }
);
console.log('\n');
@ -252,4 +254,3 @@ testRealScenario()
console.error('❌ Test failed:', error);
process.exit(1);
});

View File

@ -15,7 +15,7 @@ const getAllowedOrigins = (): string[] | boolean => {
console.error(' Multiple origins: FRONTEND_URL=https://app1.com,https://app2.com');
return [];
} else {
// Development fallback: allow localhost:3000
// Dev fallback: allow localhost:3000
console.warn('⚠️ WARNING: FRONTEND_URL not set. Defaulting to http://localhost:3000 for development.');
console.warn(' To avoid this warning, set FRONTEND_URL=http://localhost:3000 in your .env file');
return ['http://localhost:3000'];

View File

@ -7,6 +7,9 @@ import { getPublicConfigurations } from '../controllers/admin.controller';
const router = Router();
const userController = new UserController();
// GET /api/v1/users - Get all users
router.get('/', authenticateToken, asyncHandler(userController.getAllUsers.bind(userController)));
// GET /api/v1/users/search?q=<email or name>
router.get('/search', authenticateToken, asyncHandler(userController.searchUsers.bind(userController)));
@ -19,6 +22,9 @@ router.get('/configurations', authenticateToken, asyncHandler(getPublicConfigura
// POST /api/v1/users/ensure - Ensure user exists in DB (create if not exists)
router.post('/ensure', authenticateToken, asyncHandler(userController.ensureUserExists.bind(userController)));
// GET /api/v1/users/:userId - Get user by ID
router.get('/:userId', authenticateToken, asyncHandler(userController.getUserById.bind(userController)));
export default router;

View File

@ -298,6 +298,7 @@ async function autoSetup(): Promise<void> {
// Step 0: Initialize secrets
console.log('🔐 Initializing secrets...');
await initializeGoogleSecretManager();
const appDomain = process.env.APP_DOMAIN || 'royalenfield.com';
// Step 1: Check and create database if needed
const wasCreated = await checkAndCreateDatabase();
@ -323,7 +324,7 @@ async function autoSetup(): Promise<void> {
console.log(' 1. Server will start automatically');
console.log(' 2. Log in via SSO');
console.log(' 3. Run this SQL to make yourself admin:');
console.log(` UPDATE users SET role = 'ADMIN' WHERE email = 'your-email@{{API_DOMAIN}}';\n`);
console.log(` UPDATE users SET role = 'ADMIN' WHERE email = 'your-email@${appDomain}';\n`);
}
} catch (error: any) {

View File

@ -10,6 +10,8 @@ import { Dealer } from '../models/Dealer';
import { Op } from 'sequelize';
import logger from '../utils/logger';
const appDomain = process.env.APP_DOMAIN || 'royalenfield.com';
interface DealerSeedData {
salesCode?: string | null;
serviceCode?: string | null;
@ -77,7 +79,7 @@ const dealersData: DealerSeedData[] = [
onBoardingCharges: null,
date: '2014-09-30',
singleFormatMonthYear: 'Sep-2014',
domainId: 'acceleratemotors.rrnagar@dealer.{{API_DOMAIN}}',
domainId: `acceleratemotors.rrnagar@dealer.${appDomain}`,
replacement: null,
terminationResignationStatus: null,
dateOfTerminationResignation: null,

View File

@ -8,6 +8,8 @@ import { sequelize } from '../config/database';
import { User } from '../models/User';
import logger from '../utils/logger';
const appDomain = process.env.APP_DOMAIN || 'royalenfield.com';
interface DealerData {
email: string;
dealerCode: string;
@ -21,7 +23,7 @@ interface DealerData {
const dealers: DealerData[] = [
{
email: 'test.2@{{API_DOMAIN}}',
email: `test.2@${appDomain}`,
dealerCode: 'RE-MH-001',
dealerName: 'Royal Motors Mumbai',
displayName: 'Royal Motors Mumbai',
@ -31,7 +33,7 @@ const dealers: DealerData[] = [
role: 'USER',
},
{
email: 'test.4@{{API_DOMAIN}}',
email: `test.4@${appDomain}`,
dealerCode: 'RE-DL-002',
dealerName: 'Delhi enfield center',
displayName: 'Delhi Enfield Center',

View File

@ -12,6 +12,8 @@ import { notificationService } from './notification.service';
import { activityService } from './activity.service';
import { tatSchedulerService } from './tatScheduler.service';
import { emitToRequestRoom } from '../realtime/socket';
const appDomain = process.env.APP_DOMAIN || 'royalenfield.com';
// Note: DealerClaimService import removed - dealer claim approvals are handled by DealerClaimApprovalService
export class ApprovalService {
@ -538,19 +540,19 @@ export class ApprovalService {
// Check if it's an auto-step by checking approverEmail or levelName
// Note: Activity Creation, E-Invoice Generation, and Credit Note Confirmation are now activity logs only, not approval steps
// These steps are processed automatically and should NOT trigger notifications
const isAutoStep = (nextLevel as any).approverEmail === 'system@{{API_DOMAIN}}'
const isAutoStep = (nextLevel as any).approverEmail === `system@${appDomain}`
|| (nextLevel as any).approverName === 'System Auto-Process'
|| (nextLevel as any).approverId === 'system';
// IMPORTANT: Skip notifications and assignment logging for system/auto-steps
// System steps are any step with system@{{API_DOMAIN}}
// System steps are any step with system@${appDomain}
// Only send notifications to real users, NOT system processes
if (!isAutoStep && (nextLevel as any).approverId && (nextLevel as any).approverId !== 'system') {
// Additional checks: ensure approverEmail and approverName are not system-related
// This prevents notifications to system accounts even if they pass other checks
const approverEmail = (nextLevel as any).approverEmail || '';
const approverName = (nextLevel as any).approverName || '';
const isSystemEmail = approverEmail.toLowerCase() === 'system@{{API_DOMAIN}}'
const isSystemEmail = approverEmail.toLowerCase() === `system@${appDomain}`
|| approverEmail.toLowerCase().includes('system');
const isSystemName = approverName.toLowerCase() === 'system auto-process'
|| approverName.toLowerCase().includes('system');

View File

@ -26,7 +26,7 @@ import { dmsIntegrationService } from './dmsIntegration.service';
import { findDealerLocally } from './dealer.service';
import logger from '../utils/logger';
const appDomain = process.env.APP_DOMAIN || 'royalenfield.com';
let workflowServiceInstance: any;
@ -171,7 +171,7 @@ export class DealerClaimService {
levelNumber: a.level,
levelName: levelName,
approverId: approverUserId || '', // Fallback to empty string if still not resolved
approverEmail: a.email,
approverEmail: `system@${appDomain}`,
approverName: a.name || a.email,
tatHours: tatHours,
// New 5-step flow: Level 5 is the final approver (Requestor Claim Approval)
@ -330,7 +330,9 @@ export class DealerClaimService {
let stepDef = null;
// Check if this is a system step by email (for backwards compatibility)
const isSystemEmail = approver.email === 'system@{{API_DOMAIN}}' || approver.email === 'finance@{{API_DOMAIN}}';
const systemEmails = [`system@${appDomain}`];
const financeEmails = [`finance@${appDomain}`];
const isSystemEmail = systemEmails.includes(approver.email) || financeEmails.includes(approver.email);
if (approver.isAdditional) {
// Additional approver - use stepName from frontend
@ -594,7 +596,7 @@ export class DealerClaimService {
});
// 2. Add Dealer (treated as Okta/internal user - sync from Okta if needed)
if (dealerEmail && dealerEmail.toLowerCase() !== 'system@{{API_DOMAIN}}') {
if (dealerEmail && dealerEmail.toLowerCase() !== `system@${appDomain}`) {
let dealerUser = await User.findOne({
where: { email: dealerEmail.toLowerCase() },
});
@ -626,7 +628,7 @@ export class DealerClaimService {
// 3. Add all approvers from approval levels (excluding system and duplicates)
const addedUserIds = new Set<string>([initiatorId]);
const systemEmails = ['system@{{API_DOMAIN}}'];
const systemEmails = [`system@${appDomain}`];
for (const level of approvalLevels) {
const approverEmail = (level as any).approverEmail?.toLowerCase();
@ -3163,7 +3165,7 @@ export class DealerClaimService {
deptLeadLevel.levelName || undefined
);
} catch (snapshotError) {
// Log error but don't fail the reopen - snapshot is for audit, not critical
// Log error but don't fail the reopen - snapshot is for audit, not essential
logger.error(`[DealerClaimService] Failed to save workflow history snapshot (non-critical):`, snapshotError);
}
@ -3286,7 +3288,7 @@ export class DealerClaimService {
previousLevel.levelName || undefined
);
} catch (snapshotError) {
// Log error but don't fail the revise - snapshot is for audit, not critical
// Log error but don't fail the revise - snapshot is for audit, not essential
logger.error(`[DealerClaimService] Failed to save workflow history snapshot (non-critical):`, snapshotError);
}

View File

@ -25,6 +25,8 @@ import { tatSchedulerService } from './tatScheduler.service';
import { DealerClaimService } from './dealerClaim.service';
import { emitToRequestRoom } from '../realtime/socket';
const appDomain = process.env.APP_DOMAIN || 'royalenfield.com';
let dealerClaimServiceInstance: any;
@ -147,7 +149,7 @@ export class DealerClaimApprovalService {
userId
);
} catch (snapshotError) {
// Log error but don't fail the approval - snapshot is for audit, not critical
// Log error but don't fail the approval - snapshot is for audit, not essential
logger.error(`[DealerClaimApproval] Failed to save approval history snapshot (non-critical):`, snapshotError);
}
}
@ -399,11 +401,11 @@ export class DealerClaimApprovalService {
const nextApproverName = (nextLevel as any).approverName || nextApproverEmail || 'approver';
// Check if it's an auto-step or system process
const isAutoStep = nextApproverEmail === 'system@{{API_DOMAIN}}'
const isAutoStep = nextApproverEmail === `system@${appDomain}`
|| (nextLevel as any).approverName === 'System Auto-Process'
|| nextApproverId === 'system';
const isSystemEmail = nextApproverEmail.toLowerCase() === 'system@{{API_DOMAIN}}'
const isSystemEmail = nextApproverEmail.toLowerCase() === `system@${appDomain}`
|| nextApproverEmail.toLowerCase().includes('system');
const isSystemName = nextApproverName.toLowerCase() === 'system auto-process'
|| nextApproverName.toLowerCase().includes('system');
@ -747,7 +749,7 @@ export class DealerClaimApprovalService {
level.levelName || undefined
);
} catch (snapshotError) {
// Log error but don't fail the rejection - snapshot is for audit, not critical
// Log error but don't fail the rejection - snapshot is for audit, not essential
logger.error(`[DealerClaimApproval] Failed to save workflow history snapshot (non-critical):`, snapshotError);
}
@ -962,4 +964,3 @@ export class DealerClaimApprovalService {
});
}
}

View File

@ -17,11 +17,7 @@ interface EmailOptions {
attachments?: any[];
}
// Hardcoded BCC addresses (temporary - for time being)
const HARDCODED_BCC: string[] = [
'{{USER_EMAIL}}',
// Add your BCC email addresses here
];
export class EmailService {
private transporter: nodemailer.Transporter | null = null;
@ -44,7 +40,7 @@ export class EmailService {
return;
}
// Production SMTP configuration
// Prod SMTP configuration
try {
this.transporter = nodemailer.createTransport({
host: smtpHost,
@ -119,24 +115,13 @@ export class EmailService {
}
const recipients = Array.isArray(options.to) ? options.to.join(', ') : options.to;
const fromAddress = process.env.EMAIL_FROM || 'RE Flow <noreply@{{API_DOMAIN}}>';
// Merge hardcoded BCC with provided BCC
let bccRecipients: string[] = [];
if (HARDCODED_BCC.length > 0) {
bccRecipients = [...HARDCODED_BCC];
}
if (options.bcc) {
const providedBcc = Array.isArray(options.bcc) ? options.bcc : [options.bcc];
bccRecipients = [...bccRecipients, ...providedBcc];
}
const finalBcc = bccRecipients.length > 0 ? bccRecipients : undefined;
const fromAddress = process.env.EMAIL_FROM || `RE Flow <noreply@${process.env.APP_DOMAIN || 'royalenfield.com'}>`;
const mailOptions = {
from: fromAddress,
to: recipients,
cc: options.cc,
bcc: finalBcc,
bcc: options.bcc || [],
subject: options.subject,
html: options.html,
attachments: options.attachments

View File

@ -289,7 +289,7 @@ export class EmailNotificationService {
}
/**
* 4. Send Rejection Notification Email (CRITICAL)
* 4. Send Rejection Notification Email (ESSENTIAL)
*/
async sendRejectionNotification(
requestData: any,
@ -298,7 +298,7 @@ export class EmailNotificationService {
rejectionReason: string
): Promise<void> {
try {
// Use override for critical emails
// Use override for high-priority emails
const canSend = await shouldSendEmailWithOverride(
initiatorData.userId,
EmailNotificationType.REQUEST_REJECTED
@ -416,7 +416,7 @@ export class EmailNotificationService {
}
/**
* 6. Send TAT Breached Email (CRITICAL)
* 6. Send TAT Breached Email (ESSENTIAL)
*/
async sendTATBreached(
requestData: any,
@ -428,7 +428,7 @@ export class EmailNotificationService {
}
): Promise<void> {
try {
// Use override for critical emails
// Use override for high-priority emails
const canSend = await shouldSendEmailWithOverride(
approverData.userId,
EmailNotificationType.TAT_BREACHED
@ -1087,7 +1087,7 @@ export class EmailNotificationService {
// Check if next approver is the recipient (initiator reviewing their own request)
const isNextApproverInitiator = proposalData.nextApproverIsInitiator ||
(nextApproverData && nextApproverData.userId === recipientData.userId);
(nextApproverData && nextApproverData.userId === recipientData.userId);
const data: DealerProposalSubmittedData = {
recipientName: recipientData.displayName || recipientData.email,
@ -1219,7 +1219,7 @@ export class EmailNotificationService {
// Check if next approver is the recipient (initiator reviewing their own request)
const isNextApproverInitiator = completionData.nextApproverIsInitiator ||
(nextApproverData && nextApproverData.userId === recipientData.userId);
(nextApproverData && nextApproverData.userId === recipientData.userId);
const data: CompletionDocumentsSubmittedData = {
recipientName: recipientData.displayName || recipientData.email,

View File

@ -23,13 +23,15 @@ interface NotificationPayload {
metadata?: any;
}
const appDomain = process.env.APP_DOMAIN || 'royalenfield.com';
class NotificationService {
private userIdToSubscriptions: Map<string, PushSubscription[]> = new Map();
configure(vapidPublicKey?: string, vapidPrivateKey?: string, mailto?: string) {
const pub = vapidPublicKey || process.env.VAPID_PUBLIC_KEY || '';
const priv = vapidPrivateKey || process.env.VAPID_PRIVATE_KEY || '';
const contact = mailto || process.env.VAPID_CONTACT || 'mailto:admin@example.com';
const contact = mailto || process.env.VAPID_CONTACT || `mailto:admin@${appDomain}`;
if (!pub || !priv) {
logger.warn('VAPID keys are not configured. Push notifications are disabled.');
return;
@ -319,12 +321,12 @@ class NotificationService {
}
// Check if email should be sent (admin + user preferences)
// Critical emails: rejection, tat_breach, breach
// emails: rejection, tat_breach, breach
const isCriticalEmail = payload.type === 'rejection' ||
payload.type === 'tat_breach' ||
payload.type === 'breach';
const shouldSend = isCriticalEmail
? await shouldSendEmailWithOverride(userId, emailType) // Critical emails
? await shouldSendEmailWithOverride(userId, emailType) // emails
: payload.type === 'assignment'
? await shouldSendEmailWithOverride(userId, emailType) // Assignment emails - use override to ensure delivery
: await shouldSendEmail(userId, emailType); // Regular emails
@ -569,7 +571,7 @@ class NotificationService {
approverData = {
userId: (rejectedLevel as any).approverId,
displayName: (rejectedLevel as any).approverName || 'Unknown Approver',
email: (rejectedLevel as any).approverEmail || 'unknown@{{API_DOMAIN}}',
email: (rejectedLevel as any).approverEmail || `unknown@${appDomain}`,
rejectedAt: (rejectedLevel as any).actionDate,
comments: (rejectedLevel as any).comments
};
@ -612,7 +614,7 @@ class NotificationService {
approverData = {
userId: (currentLevel as any).approverId,
displayName: (currentLevel as any).approverName || 'Unknown Approver',
email: (currentLevel as any).approverEmail || 'unknown@{{API_DOMAIN}}'
email: (currentLevel as any).approverEmail || `unknown@${appDomain}`
};
}
}
@ -699,7 +701,7 @@ class NotificationService {
approverData = {
userId: (currentLevel as any).approverId,
displayName: (currentLevel as any).approverName || 'Unknown Approver',
email: (currentLevel as any).approverEmail || 'unknown@{{API_DOMAIN}}'
email: (currentLevel as any).approverEmail || `unknown@${appDomain}`
};
}
}
@ -833,7 +835,7 @@ class NotificationService {
recipientData = {
userId: (pausedLevel as any).approverId,
displayName: (pausedLevel as any).approverName || 'Unknown Approver',
email: (pausedLevel as any).approverEmail || 'unknown@{{API_DOMAIN}}'
email: (pausedLevel as any).approverEmail || `unknown@${appDomain}`
};
}
}
@ -885,7 +887,7 @@ class NotificationService {
recipientData = {
userId: (currentLevel as any).approverId,
displayName: (currentLevel as any).approverName || 'Unknown User',
email: (currentLevel as any).approverEmail || 'unknown@{{API_DOMAIN}}'
email: (currentLevel as any).approverEmail || `unknown@${appDomain}`
};
}
} else {

View File

@ -71,7 +71,7 @@ export class PdfService {
private getInvoiceHtmlTemplate(data: any): string {
const { request, invoice, dealer, claimDetails } = data;
const qrImage = invoice.qrImage ? `data:image/png;base64,${invoice.qrImage}` : '';
const logoUrl = '{{LOGO_URL}}';
const logoUrl = `{{LOGO_URL}}`;
return `
<!DOCTYPE html>
@ -119,7 +119,7 @@ export class PdfService {
<div class="info-grid">
<div class="info-section">
<div class="info-row"><div class="info-label">Customer Name</div><div class="info-value">Royal Enfield</div></div>
<div class="info-row"><div class="info-label">Customer GSTIN</div><div class="info-value">{{BUYER_GSTIN}}</div></div>
<div class="info-row"><div class="info-label">Customer GSTIN</div><div class="info-value">` + `{{BUYER_GSTIN}}` + `</div></div>
<div class="info-row"><div class="info-label">Customer Address</div><div class="info-value">State Highway 48, Vallam Industrial Corridor, Vallakottai Chennai, Tamil Nadu - 631604</div></div>
<br/>
<div class="info-row"><div class="info-label">Vehicle Owner</div><div class="info-value">N/A</div></div>

View File

@ -12,6 +12,8 @@ import { DealerClaimDetails } from '../models/DealerClaimDetails';
* PWC E-Invoice Integration Service
* Handles communication with PWC API for signed invoice generation
*/
const appDomain = process.env.APP_DOMAIN || 'royalenfield.com';
export class PWCIntegrationService {
private apiUrl: string;
private customerId: string;
@ -123,7 +125,7 @@ export class PWCIntegrationService {
SourceSystem: "RE_WORKFLOW",
is_irn: "Y",
is_ewb: "N",
email: (request as any).initiator?.email || "system@{{API_DOMAIN}}",
email: (request as any).initiator?.email || `system@${appDomain}`,
TranDtls: {
TaxSch: "GST",
SubType: "SUPPLY",

View File

@ -108,10 +108,10 @@ export class TatSchedulerService {
// Just ensure they have minimum spacing for BullMQ reliability
spacedDelay = Math.max(job.delay, 5000) + (jobIndex * 5000);
} else if (hasCollision) {
// Production with collision: add 5-minute spacing
// Prod with collision: add 5-minute spacing
spacedDelay = job.delay + (jobIndex * 300000);
} else {
// Production without collision: use calculated delays
// Prod without collision: use calculated delays
spacedDelay = job.delay;
}

View File

@ -239,7 +239,7 @@ export class TemplateService {
});
} catch (error) {
logger.error('[TemplateService] Error incrementing usage count:', error);
// Don't throw - this is not critical
// Don't throw - this is not essential
}
}
}

View File

@ -141,7 +141,7 @@ export class UserService {
});
}
async searchUsers(query: string, limit: number = 10, excludeUserId?: string): Promise<any[]> {
async searchUsers(query: string, limit: number = 10, excludeUserId?: string, source: 'local' | 'okta' | 'default' = 'default'): Promise<any[]> {
const q = (query || '').trim();
if (!q) {
return [];
@ -160,6 +160,11 @@ export class UserService {
}
}
// If source is strictly 'local', skip Okta and search DB directly
if (source === 'local') {
return await this.searchUsersLocal(q, limit, excludeUserId);
}
// Search Okta users
try {
const oktaDomain = process.env.OKTA_DOMAIN;

View File

@ -118,7 +118,7 @@ export class WorkflowService {
requestId,
requestNumber,
url: `/request/${requestNumber}`,
type: 'assignment', // CRITICAL: Differentiates from 'spectator_added' - triggers approval request email
type: 'assignment', //: Differentiates from 'spectator_added' - triggers approval request email
priority: 'HIGH',
actionRequired: true // Approvers need to take action
});
@ -506,7 +506,7 @@ export class WorkflowService {
requestId,
requestNumber: (workflow as any).requestNumber,
url: `/request/${(workflow as any).requestNumber}`,
type: 'assignment', // CRITICAL: This triggers the approval request email notification
type: 'assignment', //: This triggers the approval request email notification
priority: 'HIGH',
actionRequired: true // Additional approvers need to take action
});
@ -597,7 +597,7 @@ export class WorkflowService {
requestId,
requestNumber,
url: `/request/${requestNumber}`,
type: 'spectator_added', // CRITICAL: Differentiates from 'assignment' - triggers spectator added email
type: 'spectator_added', //: Differentiates from 'assignment' - triggers spectator added email
priority: 'MEDIUM', // Lower priority than approvers (no action required)
metadata: {
addedBy: addedBy // Used in email to show who added the spectator
@ -3016,7 +3016,7 @@ export class WorkflowService {
if (submissionDate && totalTatHours > 0) {
// Calculate total elapsed hours by summing elapsed hours from all levels
// CRITICAL: Only count elapsed hours from completed levels + current active level
//: Only count elapsed hours from completed levels + current active level
// Waiting levels (future steps) should contribute 0 elapsed hours
// This ensures that when in step 1, only step 1's elapsed hours are counted
let totalElapsedHours = 0;
@ -3033,7 +3033,7 @@ export class WorkflowService {
// Skipped levels don't contribute to elapsed time
continue;
} else if (status === 'PENDING' || status === 'IN_PROGRESS' || status === 'PAUSED') {
// CRITICAL: Only count elapsed hours for the CURRENT active level
//: Only count elapsed hours for the CURRENT active level
// Waiting levels (future steps) should NOT contribute elapsed hours
// This ensures request-level elapsed time matches the current step's elapsed time
const isCurrentLevel = approvalLevelNumber === workflowCurrentLevelNumber;

View File

@ -12,10 +12,11 @@ export const sanitizeHtml = (html: string): string => {
// Custom options can be added here if we need to allow specific tags or attributes
// For now, using default options which are quite secure
// Custom options to restrict allowed tags
// By NOT spreading ...whiteList, we explicitly only allow what we define
const options = {
whiteList: {
...whiteList,
// Add any specific tags or attributes required by the frontend
// Add only specific tags or attributes required by the frontend
'span': ['style', 'class'],
'div': ['style', 'class'],
'p': ['style', 'class'],
@ -35,7 +36,9 @@ export const sanitizeHtml = (html: string): string => {
'h5': ['style', 'class'],
'h6': ['style', 'class'],
'blockquote': ['style', 'class'],
}
},
stripIgnoreTag: true,
stripIgnoreTagBody: ['script']
};
const xssFilter = new FilterXSS(options);

View File

@ -21,7 +21,7 @@ let workingHoursCacheExpiry: Date | null = null;
*/
async function loadWorkingHoursCache(): Promise<void> {
try {
// Reload cache every 5 minutes (shorter than holidays since it's more critical)
// Reload cache every 5 minutes (shorter than holidays since it's more important)
if (workingHoursCacheExpiry && new Date() < workingHoursCacheExpiry) {
return;
}
@ -255,7 +255,7 @@ export async function addWorkingHoursExpress(start: Date | string, hoursToAdd: n
let hoursCounted = 0;
// Add whole hours
// CRITICAL: For EXPRESS, count ALL days (weekends, holidays) - only check working hours (9 AM - 6 PM)
//: For EXPRESS, count ALL days (weekends, holidays) - only check working hours (9 AM - 6 PM)
let iterations = 0;
const maxIterations = 10000; // Safety limit
@ -365,7 +365,7 @@ export function addWorkingHoursSync(start: Date | string, hoursToAdd: number): D
const hour = current.hour();
// Simple check without holidays (but respects configured working hours)
if (day >= config.startDay && day <= config.endDay &&
hour >= config.startHour && hour < config.endHour) {
hour >= config.startHour && hour < config.endHour) {
remaining -= 1;
}
}
@ -621,7 +621,7 @@ export async function calculateElapsedWorkingHours(
endDay: TAT_CONFIG.WORK_END_DAY
};
// CRITICAL: For resumed levels, we must use the exact resume time as start
//: For resumed levels, we must use the exact resume time as start
// Do NOT advance resume time to next working period - resume time is the actual moment TAT resumed
// Only advance if we're calculating from original start (not resumed)
const isResumedLevel = resumeTime !== null;
@ -631,7 +631,7 @@ export async function calculateElapsedWorkingHours(
// For resumed levels, use exact resume time (even if outside working hours)
// The working hours calculation below will handle skipping non-working periods
// CRITICAL FIX: If start time is outside working hours, advance to next working period
// FIX: If start time is outside working hours, advance to next working period
// This ensures we only count elapsed time when TAT is actually running
const originalStart = start.format('YYYY-MM-DD HH:mm:ss');