added jenkinsfile
This commit is contained in:
parent
8d34defbd7
commit
f3109f2b47
325
Jenkinsfile
vendored
Normal file
325
Jenkinsfile
vendored
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
|
||||||
|
environment {
|
||||||
|
SSH_CREDENTIALS = 'cloudtopiaa'
|
||||||
|
REMOTE_SERVER = 'ubuntu@160.187.166.17'
|
||||||
|
PROJECT_NAME = 'Royal-Enfield-Backend'
|
||||||
|
DEPLOY_PATH = '/home/ubuntu/Royal-Enfield/Re_Backend'
|
||||||
|
GIT_CREDENTIALS = 'git-cred'
|
||||||
|
REPO_URL = 'https://git.tech4biz.wiki/yasha/cognitive-prism-academia-backend.git'
|
||||||
|
GIT_BRANCH = 'main'
|
||||||
|
NPM_PATH = '/home/ubuntu/.nvm/versions/node/v22.21.1/bin/npm'
|
||||||
|
NODE_PATH = '/home/ubuntu/.nvm/versions/node/v22.21.1/bin/node'
|
||||||
|
PM2_PATH = '/home/ubuntu/.nvm/versions/node/v22.21.1/bin/pm2'
|
||||||
|
PM2_APP_NAME = 'royal-enfield-backend'
|
||||||
|
APP_PORT = '5000'
|
||||||
|
EMAIL_RECIPIENT = 'laxman.halaki@tech4biz.org'
|
||||||
|
}
|
||||||
|
|
||||||
|
options {
|
||||||
|
timeout(time: 20, unit: 'MINUTES')
|
||||||
|
disableConcurrentBuilds()
|
||||||
|
timestamps()
|
||||||
|
buildDiscarder(logRotator(numToKeepStr: '10', daysToKeepStr: '30'))
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Pre-deployment Check') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
echo "═══════════════════════════════════════════"
|
||||||
|
echo "🚀 Starting ${PROJECT_NAME} Deployment"
|
||||||
|
echo "═══════════════════════════════════════════"
|
||||||
|
echo "Server: ${REMOTE_SERVER}"
|
||||||
|
echo "Deploy Path: ${DEPLOY_PATH}"
|
||||||
|
echo "PM2 App: ${PM2_APP_NAME}"
|
||||||
|
echo "Build #: ${BUILD_NUMBER}"
|
||||||
|
echo "═══════════════════════════════════════════"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Pull Latest Code') {
|
||||||
|
steps {
|
||||||
|
sshagent(credentials: [SSH_CREDENTIALS]) {
|
||||||
|
withCredentials([usernamePassword(credentialsId: GIT_CREDENTIALS, usernameVariable: 'GIT_USER', passwordVariable: 'GIT_PASS')]) {
|
||||||
|
sh """
|
||||||
|
ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${REMOTE_SERVER} << 'ENDSSH'
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "📦 Git Operations..."
|
||||||
|
|
||||||
|
if [ -d "${DEPLOY_PATH}/.git" ]; then
|
||||||
|
cd ${DEPLOY_PATH}
|
||||||
|
|
||||||
|
echo "Configuring git..."
|
||||||
|
git config --global --add safe.directory ${DEPLOY_PATH}
|
||||||
|
git config credential.helper store
|
||||||
|
|
||||||
|
echo "Fetching updates..."
|
||||||
|
git fetch https://${GIT_USER}:${GIT_PASS}@git.tech4biz.wiki/yasha/cognitive-prism-academia-backend.git ${GIT_BRANCH}
|
||||||
|
|
||||||
|
CURRENT_COMMIT=\$(git rev-parse HEAD)
|
||||||
|
LATEST_COMMIT=\$(git rev-parse FETCH_HEAD)
|
||||||
|
|
||||||
|
if [ "\$CURRENT_COMMIT" = "\$LATEST_COMMIT" ]; then
|
||||||
|
echo "⚠️ Already up to date. No changes to deploy."
|
||||||
|
echo "Current: \$CURRENT_COMMIT"
|
||||||
|
else
|
||||||
|
echo "Pulling new changes..."
|
||||||
|
git reset --hard FETCH_HEAD
|
||||||
|
git clean -fd
|
||||||
|
echo "✓ Updated from \${CURRENT_COMMIT:0:7} to \${LATEST_COMMIT:0:7}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Cloning repository..."
|
||||||
|
rm -rf ${DEPLOY_PATH}
|
||||||
|
mkdir -p /home/ubuntu/Royal-Enfield
|
||||||
|
cd /home/ubuntu/Royal-Enfield
|
||||||
|
git clone https://${GIT_USER}:${GIT_PASS}@git.tech4biz.wiki/yasha/cognitive-prism-academia-backend.git Re_Backend
|
||||||
|
cd ${DEPLOY_PATH}
|
||||||
|
git checkout ${GIT_BRANCH}
|
||||||
|
git config --global --add safe.directory ${DEPLOY_PATH}
|
||||||
|
echo "✓ Repository cloned successfully"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd ${DEPLOY_PATH}
|
||||||
|
echo "Current commit: \$(git log -1 --oneline)"
|
||||||
|
ENDSSH
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Install Dependencies') {
|
||||||
|
steps {
|
||||||
|
sshagent(credentials: [SSH_CREDENTIALS]) {
|
||||||
|
sh """
|
||||||
|
ssh -o StrictHostKeyChecking=no ${REMOTE_SERVER} << 'ENDSSH'
|
||||||
|
set -e
|
||||||
|
export PATH="/home/ubuntu/.nvm/versions/node/v22.21.1/bin:\$PATH"
|
||||||
|
cd ${DEPLOY_PATH}
|
||||||
|
|
||||||
|
echo "🔧 Environment Check..."
|
||||||
|
echo "Node: \$(${NODE_PATH} -v)"
|
||||||
|
echo "NPM: \$(${NPM_PATH} -v)"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "📥 Installing Dependencies..."
|
||||||
|
${NPM_PATH} install --prefer-offline --no-audit --progress=false
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Dependencies installed successfully!"
|
||||||
|
ENDSSH
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Build Application') {
|
||||||
|
steps {
|
||||||
|
sshagent(credentials: [SSH_CREDENTIALS]) {
|
||||||
|
sh """
|
||||||
|
ssh -o StrictHostKeyChecking=no ${REMOTE_SERVER} << 'ENDSSH'
|
||||||
|
set -e
|
||||||
|
export PATH="/home/ubuntu/.nvm/versions/node/v22.21.1/bin:\$PATH"
|
||||||
|
cd ${DEPLOY_PATH}
|
||||||
|
|
||||||
|
echo "🔨 Building application..."
|
||||||
|
${NPM_PATH} run build
|
||||||
|
echo "✅ Build completed successfully!"
|
||||||
|
ENDSSH
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Stop PM2 Process') {
|
||||||
|
steps {
|
||||||
|
sshagent(credentials: [SSH_CREDENTIALS]) {
|
||||||
|
sh """
|
||||||
|
ssh -o StrictHostKeyChecking=no ${REMOTE_SERVER} << 'ENDSSH'
|
||||||
|
set -e
|
||||||
|
export PATH="/home/ubuntu/.nvm/versions/node/v22.21.1/bin:\$PATH"
|
||||||
|
|
||||||
|
echo "🛑 Stopping existing PM2 process..."
|
||||||
|
|
||||||
|
if ${PM2_PATH} list | grep -q "${PM2_APP_NAME}"; then
|
||||||
|
echo "Stopping ${PM2_APP_NAME}..."
|
||||||
|
${PM2_PATH} stop ${PM2_APP_NAME} || true
|
||||||
|
${PM2_PATH} delete ${PM2_APP_NAME} || true
|
||||||
|
echo "✓ Process stopped"
|
||||||
|
else
|
||||||
|
echo "No existing process found"
|
||||||
|
fi
|
||||||
|
ENDSSH
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Start with PM2') {
|
||||||
|
steps {
|
||||||
|
sshagent(credentials: [SSH_CREDENTIALS]) {
|
||||||
|
sh """
|
||||||
|
ssh -o StrictHostKeyChecking=no ${REMOTE_SERVER} << 'ENDSSH'
|
||||||
|
set -e
|
||||||
|
export PATH="/home/ubuntu/.nvm/versions/node/v22.21.1/bin:\$PATH"
|
||||||
|
cd ${DEPLOY_PATH}
|
||||||
|
|
||||||
|
echo "🚀 Starting application with PM2..."
|
||||||
|
|
||||||
|
# Start with PM2
|
||||||
|
${PM2_PATH} start ${NPM_PATH} --name "${PM2_APP_NAME}" -- start
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "⏳ Waiting for application to start..."
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Save PM2 configuration
|
||||||
|
${PM2_PATH} save
|
||||||
|
|
||||||
|
# Show PM2 status
|
||||||
|
echo ""
|
||||||
|
echo "📊 PM2 Process Status:"
|
||||||
|
${PM2_PATH} list
|
||||||
|
|
||||||
|
# Show logs (last 20 lines)
|
||||||
|
echo ""
|
||||||
|
echo "📝 Application Logs:"
|
||||||
|
${PM2_PATH} logs ${PM2_APP_NAME} --lines 20 --nostream || true
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Application started successfully!"
|
||||||
|
ENDSSH
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Health Check') {
|
||||||
|
steps {
|
||||||
|
sshagent(credentials: [SSH_CREDENTIALS]) {
|
||||||
|
sh """
|
||||||
|
ssh -o StrictHostKeyChecking=no ${REMOTE_SERVER} << 'ENDSSH'
|
||||||
|
set -e
|
||||||
|
export PATH="/home/ubuntu/.nvm/versions/node/v22.21.1/bin:\$PATH"
|
||||||
|
|
||||||
|
echo "🔍 Deployment Verification..."
|
||||||
|
|
||||||
|
# Check if PM2 process is running
|
||||||
|
if ${PM2_PATH} list | grep -q "${PM2_APP_NAME}.*online"; then
|
||||||
|
echo "✓ PM2 process is running"
|
||||||
|
else
|
||||||
|
echo "✗ PM2 process is NOT running!"
|
||||||
|
${PM2_PATH} logs ${PM2_APP_NAME} --lines 50 --nostream || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if port is listening
|
||||||
|
echo ""
|
||||||
|
echo "Checking if port ${APP_PORT} is listening..."
|
||||||
|
if ss -tuln | grep -q ":${APP_PORT} "; then
|
||||||
|
echo "✓ Application is listening on port ${APP_PORT}"
|
||||||
|
else
|
||||||
|
echo "⚠️ Port ${APP_PORT} not detected (may take a moment to start)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Show process info
|
||||||
|
echo ""
|
||||||
|
echo "📊 Process Information:"
|
||||||
|
${PM2_PATH} info ${PM2_APP_NAME}
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "═══════════════════════════════════════════"
|
||||||
|
echo "✅ DEPLOYMENT SUCCESSFUL"
|
||||||
|
echo "═══════════════════════════════════════════"
|
||||||
|
ENDSSH
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
cleanWs()
|
||||||
|
}
|
||||||
|
success {
|
||||||
|
script {
|
||||||
|
def duration = currentBuild.durationString.replace(' and counting', '')
|
||||||
|
mail to: "${EMAIL_RECIPIENT}",
|
||||||
|
subject: "✅ ${PROJECT_NAME} - Deployment Successful #${BUILD_NUMBER}",
|
||||||
|
body: """
|
||||||
|
Deployment completed successfully!
|
||||||
|
|
||||||
|
Project: ${PROJECT_NAME}
|
||||||
|
Build: #${BUILD_NUMBER}
|
||||||
|
Duration: ${duration}
|
||||||
|
Server: ${REMOTE_SERVER}
|
||||||
|
PM2 App: ${PM2_APP_NAME}
|
||||||
|
Port: ${APP_PORT}
|
||||||
|
|
||||||
|
Deployed at: ${new Date().format('yyyy-MM-dd HH:mm:ss')}
|
||||||
|
|
||||||
|
Console: ${BUILD_URL}console
|
||||||
|
|
||||||
|
Commands to manage:
|
||||||
|
- View logs: pm2 logs ${PM2_APP_NAME}
|
||||||
|
- Restart: pm2 restart ${PM2_APP_NAME}
|
||||||
|
- Stop: pm2 stop ${PM2_APP_NAME}
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
failure {
|
||||||
|
script {
|
||||||
|
sshagent(credentials: [SSH_CREDENTIALS]) {
|
||||||
|
try {
|
||||||
|
def logs = sh(
|
||||||
|
script: """ssh -o StrictHostKeyChecking=no ${REMOTE_SERVER} '
|
||||||
|
export PATH="/home/ubuntu/.nvm/versions/node/v22.21.1/bin:\$PATH"
|
||||||
|
${PM2_PATH} logs ${PM2_APP_NAME} --lines 50 --nostream || echo "No logs available"
|
||||||
|
'""",
|
||||||
|
returnStdout: true
|
||||||
|
).trim()
|
||||||
|
|
||||||
|
mail to: "${EMAIL_RECIPIENT}",
|
||||||
|
subject: "❌ ${PROJECT_NAME} - Deployment Failed #${BUILD_NUMBER}",
|
||||||
|
body: """
|
||||||
|
Deployment FAILED!
|
||||||
|
|
||||||
|
Project: ${PROJECT_NAME}
|
||||||
|
Build: #${BUILD_NUMBER}
|
||||||
|
Server: ${REMOTE_SERVER}
|
||||||
|
Failed at: ${new Date().format('yyyy-MM-dd HH:mm:ss')}
|
||||||
|
|
||||||
|
Console Log: ${BUILD_URL}console
|
||||||
|
|
||||||
|
Recent PM2 Logs:
|
||||||
|
${logs}
|
||||||
|
|
||||||
|
Action required immediately!
|
||||||
|
"""
|
||||||
|
} catch (Exception e) {
|
||||||
|
mail to: "${EMAIL_RECIPIENT}",
|
||||||
|
subject: "❌ ${PROJECT_NAME} - Deployment Failed #${BUILD_NUMBER}",
|
||||||
|
body: """
|
||||||
|
Deployment FAILED!
|
||||||
|
|
||||||
|
Project: ${PROJECT_NAME}
|
||||||
|
Build: #${BUILD_NUMBER}
|
||||||
|
Server: ${REMOTE_SERVER}
|
||||||
|
Failed at: ${new Date().format('yyyy-MM-dd HH:mm:ss')}
|
||||||
|
|
||||||
|
Console Log: ${BUILD_URL}console
|
||||||
|
|
||||||
|
Could not retrieve PM2 logs. Please check manually.
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,2 +1,2 @@
|
|||||||
import{a as t}from"./index-CUSr_BWL.js";import"./radix-vendor-CbkudDDo.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-i7LKlA3D.js";import"./socket-vendor-TjCxX7sJ.js";import"./router-vendor-1fSSvDCY.js";async function l(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function m(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function d(n){return(await t.get(`/conclusions/${n}`)).data.data}export{m as finalizeConclusion,l as generateConclusion,d as getConclusion};
|
import{a as t}from"./index-Leqyafa0.js";import"./radix-vendor-CbkudDDo.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-i7LKlA3D.js";import"./socket-vendor-TjCxX7sJ.js";import"./router-vendor-1fSSvDCY.js";async function l(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function m(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function d(n){return(await t.get(`/conclusions/${n}`)).data.data}export{m as finalizeConclusion,l as generateConclusion,d as getConclusion};
|
||||||
//# sourceMappingURL=conclusionApi-CGoEAD9_.js.map
|
//# sourceMappingURL=conclusionApi-BOHzpMlV.js.map
|
||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"conclusionApi-CGoEAD9_.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 */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion"],"mappings":"0PAwBA,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,CAKA,eAAsBC,EAAcJ,EAA8C,CAEhF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB"}
|
{"version":3,"file":"conclusionApi-BOHzpMlV.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 */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion"],"mappings":"0PAwBA,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,CAKA,eAAsBC,EAAcJ,EAA8C,CAEhF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB"}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
build/assets/index-Leqyafa0.js.map
Normal file
1
build/assets/index-Leqyafa0.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -2,6 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self' blob:; style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self' data: https: blob:; connect-src 'self' blob: data:; frame-src 'self' blob:; object-src 'none'; base-uri 'self'; form-action 'self';" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/royal_enfield_logo.svg" />
|
<link rel="icon" type="image/svg+xml" href="/royal_enfield_logo.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="description" content="Royal Enfield Approval & Request Management Portal - Streamlined approval workflows for enterprise operations" />
|
<meta name="description" content="Royal Enfield Approval & Request Management Portal - Streamlined approval workflows for enterprise operations" />
|
||||||
@ -50,7 +51,7 @@
|
|||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/index-CUSr_BWL.js"></script>
|
<script type="module" crossorigin src="/assets/index-Leqyafa0.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-Cji9-Yri.js">
|
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-Cji9-Yri.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CbkudDDo.js">
|
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CbkudDDo.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DHm03ykU.js">
|
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DHm03ykU.js">
|
||||||
|
|||||||
60
src/app.ts
60
src/app.ts
@ -43,16 +43,51 @@ if (process.env.TRUST_PROXY === 'true' || process.env.NODE_ENV === 'production')
|
|||||||
app.use(corsMiddleware);
|
app.use(corsMiddleware);
|
||||||
|
|
||||||
// Security middleware - Configure Helmet to work with CORS
|
// Security middleware - Configure Helmet to work with CORS
|
||||||
|
// Get frontend URL for CSP - allow cross-origin connections in development
|
||||||
|
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
|
||||||
|
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
|
// Build connect-src directive - allow backend API and blob URLs
|
||||||
|
const connectSrc = ["'self'", "blob:", "data:"];
|
||||||
|
if (isDevelopment) {
|
||||||
|
// In development, allow connections to common dev ports
|
||||||
|
connectSrc.push("http://localhost:3000", "http://localhost:5000", "ws://localhost:3000", "ws://localhost:5000");
|
||||||
|
// Also allow the configured frontend URL if it's a localhost URL
|
||||||
|
if (frontendUrl.includes('localhost')) {
|
||||||
|
connectSrc.push(frontendUrl);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// In production, only allow the configured frontend URL
|
||||||
|
if (frontendUrl && frontendUrl !== '*') {
|
||||||
|
const frontendOrigins = frontendUrl.split(',').map(url => url.trim()).filter(Boolean);
|
||||||
|
connectSrc.push(...frontendOrigins);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build CSP directives - conditionally include upgradeInsecureRequests
|
||||||
|
const cspDirectives: any = {
|
||||||
|
defaultSrc: ["'self'", "blob:"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
|
||||||
|
scriptSrc: ["'self'"],
|
||||||
|
imgSrc: ["'self'", "data:", "https:", "blob:"],
|
||||||
|
connectSrc: connectSrc,
|
||||||
|
frameSrc: ["'self'", "blob:"],
|
||||||
|
fontSrc: ["'self'", "https://fonts.gstatic.com", "data:"],
|
||||||
|
objectSrc: ["'none'"],
|
||||||
|
baseUri: ["'self'"],
|
||||||
|
formAction: ["'self'"],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only add upgradeInsecureRequests in production (it forces HTTPS)
|
||||||
|
if (!isDevelopment) {
|
||||||
|
cspDirectives.upgradeInsecureRequests = [];
|
||||||
|
}
|
||||||
|
|
||||||
app.use(helmet({
|
app.use(helmet({
|
||||||
crossOriginEmbedderPolicy: false,
|
crossOriginEmbedderPolicy: false,
|
||||||
crossOriginResourcePolicy: { policy: "cross-origin" },
|
crossOriginResourcePolicy: { policy: "cross-origin" },
|
||||||
contentSecurityPolicy: {
|
contentSecurityPolicy: {
|
||||||
directives: {
|
directives: cspDirectives,
|
||||||
defaultSrc: ["'self'"],
|
|
||||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
||||||
scriptSrc: ["'self'"],
|
|
||||||
imgSrc: ["'self'", "data:", "https:"],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -189,8 +224,16 @@ if (fs.existsSync(buildPath)) {
|
|||||||
|
|
||||||
// Serve static files if React build exists
|
// Serve static files if React build exists
|
||||||
if (reactBuildPath && fs.existsSync(path.join(reactBuildPath, "index.html"))) {
|
if (reactBuildPath && fs.existsSync(path.join(reactBuildPath, "index.html"))) {
|
||||||
// Serve static assets (JS, CSS, images, etc.)
|
// Serve static assets (JS, CSS, images, etc.) - these will have CSP headers from Helmet
|
||||||
app.use(express.static(reactBuildPath));
|
app.use(express.static(reactBuildPath, {
|
||||||
|
setHeaders: (res: express.Response, filePath: string) => {
|
||||||
|
// Apply CSP headers to HTML files served as static files
|
||||||
|
if (filePath.endsWith('.html')) {
|
||||||
|
// CSP headers are already set by Helmet middleware, but ensure they're applied
|
||||||
|
// The meta tag in index.html will also enforce CSP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
// Catch-all handler: serve React app for all non-API routes
|
// Catch-all handler: serve React app for all non-API routes
|
||||||
// This must be AFTER all API routes to avoid intercepting API requests
|
// This must be AFTER all API routes to avoid intercepting API requests
|
||||||
@ -207,6 +250,7 @@ if (reactBuildPath && fs.existsSync(path.join(reactBuildPath, "index.html"))) {
|
|||||||
|
|
||||||
// Serve React app for all other routes (SPA routing)
|
// Serve React app for all other routes (SPA routing)
|
||||||
// This handles client-side routing in React Router
|
// This handles client-side routing in React Router
|
||||||
|
// CSP headers from Helmet will be applied to this response
|
||||||
res.sendFile(path.join(reactBuildPath!, "index.html"));
|
res.sendFile(path.join(reactBuildPath!, "index.html"));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -5,16 +5,21 @@ const getAllowedOrigins = (): string[] | boolean => {
|
|||||||
const frontendUrl = process.env.FRONTEND_URL;
|
const frontendUrl = process.env.FRONTEND_URL;
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
// FRONTEND_URL is required - no fallbacks
|
// In development, if FRONTEND_URL is not set, default to localhost:3000
|
||||||
if (!frontendUrl) {
|
if (!frontendUrl) {
|
||||||
|
if (isProduction) {
|
||||||
console.error('❌ ERROR: FRONTEND_URL environment variable is not set!');
|
console.error('❌ ERROR: FRONTEND_URL environment variable is not set!');
|
||||||
console.error(' CORS will block all origins. This will prevent frontend connections.');
|
console.error(' CORS will block all origins. This will prevent frontend connections.');
|
||||||
console.error(' To fix: Set FRONTEND_URL environment variable with your frontend URL(s)');
|
console.error(' To fix: Set FRONTEND_URL environment variable with your frontend URL(s)');
|
||||||
console.error(' Example: FRONTEND_URL=https://your-frontend-domain.com');
|
console.error(' Example: FRONTEND_URL=https://your-frontend-domain.com');
|
||||||
console.error(' Multiple origins: FRONTEND_URL=https://app1.com,https://app2.com');
|
console.error(' Multiple origins: FRONTEND_URL=https://app1.com,https://app2.com');
|
||||||
console.error(' Development: FRONTEND_URL=http://localhost:3000');
|
|
||||||
// Return empty array to block all origins if not configured
|
|
||||||
return [];
|
return [];
|
||||||
|
} else {
|
||||||
|
// Development 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'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If FRONTEND_URL is set to '*', allow all origins
|
// If FRONTEND_URL is set to '*', allow all origins
|
||||||
@ -30,7 +35,7 @@ const getAllowedOrigins = (): string[] | boolean => {
|
|||||||
|
|
||||||
if (origins.length === 0) {
|
if (origins.length === 0) {
|
||||||
console.error('❌ ERROR: FRONTEND_URL is set but contains no valid URLs!');
|
console.error('❌ ERROR: FRONTEND_URL is set but contains no valid URLs!');
|
||||||
return [];
|
return isProduction ? [] : ['http://localhost:3000']; // Fallback for development
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`✅ CORS: Allowing origins from FRONTEND_URL: ${origins.join(', ')}`);
|
console.log(`✅ CORS: Allowing origins from FRONTEND_URL: ${origins.join(', ')}`);
|
||||||
|
|||||||
@ -209,6 +209,14 @@ router.get('/documents/:documentId/preview',
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set CORS headers to allow blob URL creation when served from same origin
|
||||||
|
const origin = req.headers.origin;
|
||||||
|
if (origin) {
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||||
|
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
||||||
|
}
|
||||||
|
res.setHeader('Access-Control-Expose-Headers', 'Content-Type, Content-Disposition');
|
||||||
|
|
||||||
// Set appropriate content type
|
// Set appropriate content type
|
||||||
res.contentType(fileType);
|
res.contentType(fileType);
|
||||||
|
|
||||||
@ -271,6 +279,14 @@ router.get('/work-notes/attachments/:attachmentId/preview',
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set CORS headers to allow blob URL creation when served from same origin
|
||||||
|
const origin = req.headers.origin;
|
||||||
|
if (origin) {
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||||
|
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
||||||
|
}
|
||||||
|
res.setHeader('Access-Control-Expose-Headers', 'Content-Type, Content-Disposition');
|
||||||
|
|
||||||
// Set appropriate content type
|
// Set appropriate content type
|
||||||
res.contentType(fileInfo.fileType);
|
res.contentType(fileInfo.fileType);
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ export const SYSTEM_EVENT_REQUEST_ID = '00000000-0000-0000-0000-000000000001';
|
|||||||
|
|
||||||
export type ActivityEntry = {
|
export type ActivityEntry = {
|
||||||
requestId: string;
|
requestId: string;
|
||||||
type: 'created' | 'assignment' | 'approval' | 'rejection' | 'status_change' | 'comment' | 'reminder' | 'document_added' | 'sla_warning' | 'ai_conclusion_generated' | 'closed' | 'login';
|
type: 'created' | 'submitted' | 'assignment' | 'approval' | 'rejection' | 'status_change' | 'comment' | 'reminder' | 'document_added' | 'sla_warning' | 'ai_conclusion_generated' | 'closed' | 'login';
|
||||||
user?: { userId: string; name?: string; email?: string };
|
user?: { userId: string; name?: string; email?: string };
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
action: string;
|
action: string;
|
||||||
@ -23,6 +23,7 @@ class ActivityService {
|
|||||||
private inferCategory(type: string): string {
|
private inferCategory(type: string): string {
|
||||||
const categoryMap: Record<string, string> = {
|
const categoryMap: Record<string, string> = {
|
||||||
'created': 'WORKFLOW',
|
'created': 'WORKFLOW',
|
||||||
|
'submitted': 'WORKFLOW',
|
||||||
'approval': 'WORKFLOW',
|
'approval': 'WORKFLOW',
|
||||||
'rejection': 'WORKFLOW',
|
'rejection': 'WORKFLOW',
|
||||||
'status_change': 'WORKFLOW',
|
'status_change': 'WORKFLOW',
|
||||||
@ -47,6 +48,7 @@ class ActivityService {
|
|||||||
'status_change': 'INFO',
|
'status_change': 'INFO',
|
||||||
'login': 'INFO',
|
'login': 'INFO',
|
||||||
'created': 'INFO',
|
'created': 'INFO',
|
||||||
|
'submitted': 'INFO',
|
||||||
'comment': 'INFO',
|
'comment': 'INFO',
|
||||||
'document_added': 'INFO',
|
'document_added': 'INFO',
|
||||||
'assignment': 'INFO',
|
'assignment': 'INFO',
|
||||||
|
|||||||
@ -1614,13 +1614,33 @@ export class WorkflowService {
|
|||||||
isDraft: false,
|
isDraft: false,
|
||||||
submissionDate: now
|
submissionDate: now
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get initiator details for activity logging
|
||||||
|
const initiatorId = (updated as any).initiatorId;
|
||||||
|
const initiator = initiatorId ? await User.findByPk(initiatorId) : null;
|
||||||
|
const initiatorName = (initiator as any)?.displayName || (initiator as any)?.email || 'User';
|
||||||
|
const workflowTitle = (updated as any).title || 'Request';
|
||||||
|
|
||||||
|
// Log submitted activity (similar to created activity in createWorkflow)
|
||||||
|
activityService.log({
|
||||||
|
requestId: (updated as any).requestId,
|
||||||
|
type: 'submitted',
|
||||||
|
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
action: 'Request submitted',
|
||||||
|
details: `Request "${workflowTitle}" submitted by ${initiatorName}`
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log status change activity
|
||||||
activityService.log({
|
activityService.log({
|
||||||
requestId: (updated as any).requestId,
|
requestId: (updated as any).requestId,
|
||||||
type: 'status_change',
|
type: 'status_change',
|
||||||
|
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
action: 'Submitted',
|
action: 'Submitted',
|
||||||
details: 'Request moved to PENDING'
|
details: 'Request moved from DRAFT to PENDING'
|
||||||
});
|
});
|
||||||
|
|
||||||
const current = await ApprovalLevel.findOne({
|
const current = await ApprovalLevel.findOne({
|
||||||
where: { requestId: (updated as any).requestId, levelNumber: (updated as any).currentLevel || 1 }
|
where: { requestId: (updated as any).requestId, levelNumber: (updated as any).currentLevel || 1 }
|
||||||
});
|
});
|
||||||
@ -1632,6 +1652,16 @@ export class WorkflowService {
|
|||||||
status: ApprovalStatus.IN_PROGRESS
|
status: ApprovalStatus.IN_PROGRESS
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Log assignment activity for the first approver (similar to createWorkflow)
|
||||||
|
activityService.log({
|
||||||
|
requestId: (updated as any).requestId,
|
||||||
|
type: 'assignment',
|
||||||
|
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
action: 'Assigned to approver',
|
||||||
|
details: `Request assigned to ${(current as any).approverName || (current as any).approverEmail || 'approver'} for review`
|
||||||
|
});
|
||||||
|
|
||||||
// Schedule TAT notification jobs for the first level
|
// Schedule TAT notification jobs for the first level
|
||||||
try {
|
try {
|
||||||
const workflowPriority = (updated as any).priority || 'STANDARD';
|
const workflowPriority = (updated as any).priority || 'STANDARD';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user