vulnnearable comments removed and source exposing to frobrowser disabled worknote XSS fixed
This commit is contained in:
parent
b32a3505ac
commit
00e1d51c66
@ -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
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
@ -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`:**
|
||||
|
||||
@ -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
@ -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};
|
||||
@ -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"}
|
||||
60
build/assets/index-5rjlVIR5.js
Normal file
60
build/assets/index-5rjlVIR5.js
Normal file
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
2
build/assets/ui-vendor-BFJfF1vG.js
Normal file
2
build/assets/ui-vendor-BFJfF1vG.js
Normal file
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
@ -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
4
build/robots.txt
Normal file
@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Disallow: /api/
|
||||
|
||||
Sitemap: https://reflow.royalenfield.com/sitemap.xml
|
||||
9
build/sitemap.xml
Normal file
9
build/sitemap.xml
Normal 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>
|
||||
@ -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
|
||||
|
||||
@ -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:**
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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": [
|
||||
|
||||
@ -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": [
|
||||
|
||||
@ -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}}"
|
||||
},
|
||||
...
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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}}';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}`,
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -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!"
|
||||
}
|
||||
```
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
);
|
||||
|
||||
|
||||
@ -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 ""
|
||||
|
||||
@ -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'",
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
|
||||
@ -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}`;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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'
|
||||
};
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
|
||||
@ -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'];
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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');
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user