Compare commits
	
		
			14 Commits
		
	
	
		
			168e909c77
			...
			375937ed5e
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 375937ed5e | ||
| 3e1a51c093 | |||
| 77d642eac1 | |||
| 5e680c3947 | |||
| cbe98e8cd0 | |||
| 72b6bb3cd6 | |||
| bb3e966daf | |||
| 09068fe731 | |||
| 47bd8610d2 | |||
| e517367967 | |||
| ee27b1b3cc | |||
| 3f3b42e2ec | |||
| 866bdc1b77 | |||
| 0ccb7dc16f | 
| @ -25,6 +25,12 @@ jobs: | ||||
|       - name: Copy .env.example to .env | ||||
|         run: cp functions/.env.example functions/.env | ||||
| 
 | ||||
|       - name: Create private key file | ||||
|         run: | | ||||
|           mkdir -p functions/assets/keys | ||||
|           echo "${{ secrets.FITLIEN_PRIVATEKEY_DEV }}" > functions/assets/keys/fitLien_private.pem | ||||
|           chmod 600 functions/assets/keys/fitLien_private.pem | ||||
| 
 | ||||
|       - name: Replace variables in .env | ||||
|         run: | | ||||
|           sed -i "s/#{TWILIO_ACCOUNT_SID}#/${{ secrets.TWILIO_ACCOUNT_SID }}/" functions/.env | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| name: Deploy FitLien services to Dev | ||||
| name: Deploy FitLien services to QA | ||||
| 
 | ||||
| on: | ||||
|   push: | ||||
| @ -25,6 +25,12 @@ jobs: | ||||
|       - name: Copy .env.example to .env | ||||
|         run: cp functions/.env.example functions/.env | ||||
| 
 | ||||
|       - name: Create private key file | ||||
|         run: | | ||||
|           mkdir -p functions/assets/keys | ||||
|           echo "${{ secrets.FITLIEN_PRIVATEKEY_DEV }}" > functions/assets/keys/fitLien_private.pem | ||||
|           chmod 600 functions/assets/keys/fitLien_private.pem | ||||
| 
 | ||||
|       - name: Replace variables in .env | ||||
|         run: | | ||||
|           sed -i "s/#{TWILIO_ACCOUNT_SID}#/${{ secrets.TWILIO_ACCOUNT_SID }}/" functions/.env | ||||
|  | ||||
							
								
								
									
										70
									
								
								.gitea/workflows/deploy.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								.gitea/workflows/deploy.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,70 @@ | ||||
| name: Deploy FitLien services | ||||
| 
 | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
| jobs: | ||||
|   deploy: | ||||
|     name: Deploy | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v2 | ||||
|         with: | ||||
|           fetch-depth: 0 | ||||
| 
 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: 22 | ||||
| 
 | ||||
|       - name: Clean install | ||||
|         run: npm clean-install | ||||
| 
 | ||||
|       - name: Copy .env.example to .env | ||||
|         run: cp functions/.env.example functions/.env | ||||
| 
 | ||||
|       - name: Create private key file | ||||
|         run: | | ||||
|           mkdir -p functions/assets/keys | ||||
|           echo "${{ secrets.FITLIEN_PRIVATEKEY_DEV }}" > functions/assets/keys/fitLien_private.pem | ||||
|           chmod 600 functions/assets/keys/fitLien_private.pem | ||||
| 
 | ||||
|       - name: Replace variables in .env | ||||
|         run: | | ||||
|           sed -i "s/#{TWILIO_ACCOUNT_SID}#/${{ secrets.TWILIO_ACCOUNT_SID }}/" functions/.env | ||||
|           sed -i "s/#{TWILIO_AUTH_TOKEN}#/${{ secrets.TWILIO_AUTH_TOKEN }}/" functions/.env | ||||
|           sed -i "s/#{TWILIO_PHONE_NUMBER}#/${{ secrets.TWILIO_PHONE_NUMBER }}/" functions/.env | ||||
|           sed -i "s/#{SERVICES_RGN}#/${{ vars.SERVICES_RGN }}/" functions/.env | ||||
|           sed -i "s/#{GOOGLE_MAPS_API_KEY}#/${{ secrets.GOOGLE_MAPS_API_KEY }}/" functions/.env | ||||
|           sed -i "s/#{SES_FROM_EMAIL}#/${{ vars.SES_FROM_EMAIL }}/" functions/.env | ||||
|           sed -i "s/#{SES_REPLY_TO_EMAIL}#/${{ vars.SES_REPLY_TO_EMAIL }}/" functions/.env | ||||
|           sed -i "s/#{AWS_ACCESS_KEY_ID}#/${{ secrets.AWS_ACCESS_KEY_ID }}/" functions/.env | ||||
|           sed -i "s/#{AWS_SECRET_ACCESS_KEY}#/${{ secrets.AWS_SECRET_ACCESS_KEY }}/" functions/.env | ||||
|           sed -i "s/#{AWS_REGION}#/${{ secrets.AWS_REGION }}/" functions/.env | ||||
|           sed -i "s/#{PHONEPE_CLIENT_ID}#/${{ secrets.PHONEPE_CLIENT_ID }}/" functions/.env | ||||
|           sed -i "s/#{PHONEPE_CLIENT_SECRET}#/${{ secrets.PHONEPE_CLIENT_SECRET }}/" functions/.env | ||||
|           sed -i "s/#{PHONEPE_API_URL}#/${{ secrets.PHONEPE_API_URL }}/" functions/.env | ||||
|           sed -i "s/#{PHONEPE_WEBHOOK_USERNAME}#/${{ secrets.PHONEPE_WEBHOOK_USERNAME }}/" functions/.env | ||||
|           sed -i "s/#{PHONEPE_WEBHOOK_PASSWORD}#/${{ secrets.PHONEPE_WEBHOOK_PASSWORD }}/" functions/.env | ||||
| 
 | ||||
|           cat functions/.env | ||||
|       - name: "Replace #{SERVICES_RGN}# in all .ts files" | ||||
|         run: | | ||||
|           find . -type f -name "*.ts" -exec sed -i "s/#{SERVICES_RGN}#/${{ vars.SERVICES_RGN }}/g" {} + | ||||
| 
 | ||||
|       - name: Build | ||||
|         run: | | ||||
|           npm install -g typescript | ||||
|           cd functions | ||||
|           npm install | ||||
|           npx tsc | ||||
|           cd .. | ||||
|           ls -la | ||||
| 
 | ||||
|       - name: Deploy | ||||
|         run: | | ||||
|           curl -sL firebase.tools | upgrade=true bash | ||||
|           firebase use --token ${{ secrets.FIREBASE_TOKEN }} release | ||||
|           firebase deploy --token "${{ secrets.FIREBASE_TOKEN }}" --force --non-interactive | ||||
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -26,6 +26,9 @@ pids | ||||
| # Directory for instrumented libs generated by jscoverage/JSCover | ||||
| lib-cov | ||||
| 
 | ||||
| # Private key | ||||
| /functions/assets/keys/fitLien_private.pem | ||||
| 
 | ||||
| # Coverage directory used by tools like istanbul | ||||
| coverage | ||||
| 
 | ||||
| @ -67,3 +70,6 @@ node_modules/ | ||||
| 
 | ||||
| # dataconnect generated files | ||||
| .dataconnect | ||||
| 
 | ||||
| .DS_Store | ||||
| **/.DS_Store | ||||
| @ -1,7 +1,8 @@ | ||||
| { | ||||
|   "firestore": { | ||||
|     "rules": "firestore.rules", | ||||
|     "indexes": "firestore.indexes.json" | ||||
|     "indexes": "firestore.indexes.json", | ||||
|     "database": "(default)" | ||||
|   }, | ||||
|   "functions": [ | ||||
|     { | ||||
|  | ||||
							
								
								
									
										27
									
								
								functions/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										27
									
								
								functions/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -25,13 +25,15 @@ | ||||
|         "node-fetch": "^2.7.0", | ||||
|         "pdfjs-dist": "^5.0.375", | ||||
|         "pdfmake": "^0.2.20", | ||||
|         "twilio": "^5.4.0" | ||||
|         "twilio": "^5.4.0", | ||||
|         "xmldom": "^0.6.0" | ||||
|       }, | ||||
|       "devDependencies": { | ||||
|         "@types/long": "^5.0.0", | ||||
|         "@types/mime-types": "^2.1.4", | ||||
|         "@types/node": "^22.13.14", | ||||
|         "@types/node": "^22.15.31", | ||||
|         "@types/pdfmake": "^0.2.11", | ||||
|         "@types/xmldom": "^0.1.34", | ||||
|         "firebase-functions-test": "^3.1.0", | ||||
|         "typescript": "^5.8.2" | ||||
|       }, | ||||
| @ -2829,9 +2831,10 @@ | ||||
|       "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" | ||||
|     }, | ||||
|     "node_modules/@types/node": { | ||||
|       "version": "22.15.2", | ||||
|       "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.2.tgz", | ||||
|       "integrity": "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==", | ||||
|       "version": "22.15.31", | ||||
|       "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.31.tgz", | ||||
|       "integrity": "sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==", | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "undici-types": "~6.21.0" | ||||
|       } | ||||
| @ -2946,6 +2949,12 @@ | ||||
|       "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "node_modules/@types/xmldom": { | ||||
|       "version": "0.1.34", | ||||
|       "resolved": "https://registry.npmjs.org/@types/xmldom/-/xmldom-0.1.34.tgz", | ||||
|       "integrity": "sha512-7eZFfxI9XHYjJJuugddV6N5YNeXgQE1lArWOcd1eCOKWb/FGs5SIjacSYuEJuwhsGS3gy4RuZ5EUIcqYscuPDA==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "node_modules/@types/yargs": { | ||||
|       "version": "17.0.33", | ||||
|       "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", | ||||
| @ -8324,6 +8333,14 @@ | ||||
|       "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", | ||||
|       "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" | ||||
|     }, | ||||
|     "node_modules/xmldom": { | ||||
|       "version": "0.6.0", | ||||
|       "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz", | ||||
|       "integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==", | ||||
|       "engines": { | ||||
|         "node": ">=10.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/y18n": { | ||||
|       "version": "5.0.8", | ||||
|       "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", | ||||
|  | ||||
| @ -32,13 +32,15 @@ | ||||
|     "node-fetch": "^2.7.0", | ||||
|     "pdfjs-dist": "^5.0.375", | ||||
|     "pdfmake": "^0.2.20", | ||||
|     "twilio": "^5.4.0" | ||||
|     "twilio": "^5.4.0", | ||||
|     "xmldom": "^0.6.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@types/long": "^5.0.0", | ||||
|     "@types/mime-types": "^2.1.4", | ||||
|     "@types/node": "^22.13.14", | ||||
|     "@types/node": "^22.15.31", | ||||
|     "@types/pdfmake": "^0.2.11", | ||||
|     "@types/xmldom": "^0.1.34", | ||||
|     "firebase-functions-test": "^3.1.0", | ||||
|     "typescript": "^5.8.2" | ||||
|   }, | ||||
|  | ||||
							
								
								
									
										7
									
								
								functions/src/dooraccess/doorAccessUser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								functions/src/dooraccess/doorAccessUser.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| export type DoorAccessUser = { | ||||
|     name: string; | ||||
|     location: string; | ||||
|     role: string; | ||||
|     expireFrom: Date | null; | ||||
|     expireTo: Date | null; | ||||
| }; | ||||
							
								
								
									
										560
									
								
								functions/src/dooraccess/essl.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										560
									
								
								functions/src/dooraccess/essl.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,560 @@ | ||||
| import { onRequest } from "firebase-functions/https"; | ||||
| import { DoorAccessUser } from "./doorAccessUser"; | ||||
| import { Request } from "firebase-functions/v2/https"; | ||||
| import { Response } from "express"; | ||||
| import { getCorsHandler } from "../shared/middleware"; | ||||
| import { getLogger } from "../shared/config"; | ||||
| import { DOMParser } from 'xmldom'; | ||||
| import { RSADecryption } from "../shared/decrypt"; | ||||
| const logger = getLogger(); | ||||
| const corsHandler = getCorsHandler(); | ||||
| 
 | ||||
| export interface EmployeeCodeRequest { | ||||
|     employeeCode: string; | ||||
| } | ||||
| 
 | ||||
| export interface PushLogRequest extends EmployeeCodeRequest { | ||||
|     attendanceDate: string; | ||||
| } | ||||
| 
 | ||||
| export interface UpdateEmployeeExRequest { | ||||
|     employeeCode: string; | ||||
|     employeeName: string; | ||||
|     employeeLocation: string; | ||||
|     employeeRole: string; | ||||
|     employeeVerificationType: string; | ||||
|     employeeExpiryFrom: string; | ||||
|     employeeExpiryTo: string; | ||||
|     employeeCardNumber: string; | ||||
|     groupId: string; | ||||
|     employeePhoto: string; | ||||
| } | ||||
| 
 | ||||
| function getDecryptedPassword(password: string | null): string { | ||||
|     if (!password) { | ||||
|         throw new Error('Password is required'); | ||||
|     } | ||||
|     return RSADecryption.decryptPassword(password); | ||||
| } | ||||
| 
 | ||||
| const escapeXml = (str: string) => { | ||||
|     return str | ||||
|         .replace(/&/g, '&') | ||||
|         .replace(/</g, '<') | ||||
|         .replace(/>/g, '>') | ||||
|         .replace(/"/g, '"') | ||||
|         .replace(/'/g, '''); | ||||
| }; | ||||
| 
 | ||||
| function createGetEmployeeDetailsRequest(username: string, password: string, employeeCode: string): string | null { | ||||
|     const soapRequest = `<?xml version="1.0" encoding="utf-8"?>
 | ||||
| <soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope"> | ||||
|   <soap12:Body> | ||||
|     <GetEmployeeDetails xmlns="http://tempuri.org/"> | ||||
|       <UserName>${escapeXml(username)}</UserName> | ||||
|       <Password>${escapeXml(password)}</Password> | ||||
|       <EmployeeCode>${escapeXml(employeeCode)}</EmployeeCode> | ||||
|     </GetEmployeeDetails> | ||||
|   </soap12:Body> | ||||
| </soap12:Envelope>`; | ||||
| 
 | ||||
|     return soapRequest; | ||||
| } | ||||
| 
 | ||||
| function parseGetEmployeeDetailsResponse(soapResponse: string): DoorAccessUser { | ||||
|     const parser = new DOMParser(); | ||||
|     const xmlDoc = parser.parseFromString(soapResponse, "text/xml"); | ||||
|     if (xmlDoc.documentElement.tagName !== 'soap:Envelope') { | ||||
|         throw new Error("Invalid SOAP response"); | ||||
|     } | ||||
|     if (null == xmlDoc.documentElement.firstChild) { | ||||
|         throw new Error("Invalid SOAP response"); | ||||
|     } | ||||
| 
 | ||||
|     let currentElement = xmlDoc.documentElement.firstChild as HTMLElement; | ||||
|     if (currentElement.tagName !== 'soap:Body') { | ||||
|         throw new Error("Invalid SOAP response"); | ||||
|     } | ||||
| 
 | ||||
|     currentElement = currentElement.firstChild as HTMLElement; | ||||
|     if (currentElement.tagName !== 'GetEmployeeDetailsResponse') { | ||||
|         throw new Error("Invalid SOAP response"); | ||||
|     } | ||||
| 
 | ||||
|     currentElement = currentElement.firstChild as HTMLElement; | ||||
|     if (currentElement.tagName !== 'GetEmployeeDetailsResult') { | ||||
|         throw new Error("Invalid SOAP response"); | ||||
|     } | ||||
| 
 | ||||
|     const resultText = currentElement.textContent; | ||||
|     if (!resultText) { | ||||
|         throw new Error("GetEmployeeDetailsResult is empty"); | ||||
|     } | ||||
| 
 | ||||
|     const userDetails: DoorAccessUser = | ||||
|     { | ||||
|         name: '', | ||||
|         location: '', | ||||
|         role: '', | ||||
|         expireFrom: null, | ||||
|         expireTo: null | ||||
|     }; | ||||
|     const pairs = resultText.split(','); | ||||
|     pairs.forEach(pair => { | ||||
|         const [key, value] = pair.split('='); | ||||
|         if (key && value !== undefined) { | ||||
|             const cleanKey = key.trim(); | ||||
|             const cleanValue = value.trim(); | ||||
|             switch (cleanKey) { | ||||
|                 case 'EmployeeName': | ||||
|                     userDetails.name = cleanValue; | ||||
|                     break; | ||||
|                 case 'EmployeeLocation': | ||||
|                     userDetails.location = cleanValue; | ||||
|                     break; | ||||
|                 case 'EmployeeRole': | ||||
|                     userDetails.role = cleanValue; | ||||
|                     break; | ||||
|                 case 'EmployeeExpiryFrom': | ||||
|                     userDetails.expireFrom = new Date(cleanValue); | ||||
|                     break; | ||||
|                 case 'EmployeeExpiryTo': | ||||
|                     userDetails.expireTo = new Date(cleanValue); | ||||
|                     break; | ||||
|                 default: | ||||
|                     break; | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     return userDetails; | ||||
| } | ||||
| 
 | ||||
| function isValidDateString(dateString: string): boolean { | ||||
|     const dateRegex = /^\d{4}\-\d{2}\-\d{2}$/; | ||||
|     return dateRegex.test(dateString); | ||||
| } | ||||
| 
 | ||||
| function createUpdateEmployeeExRequest(username: string, password: string, request: UpdateEmployeeExRequest): string | null { | ||||
| 
 | ||||
|     if (!username || !password || !request.employeeCode || !request.employeeName || !request.employeeLocation) { | ||||
|         throw new Error('Missing required fields'); | ||||
|     } | ||||
| 
 | ||||
|     if (!isValidDateString(request.employeeExpiryFrom) || !isValidDateString(request.employeeExpiryTo)) { | ||||
|         throw new Error('Invalid date format'); | ||||
|     } | ||||
| 
 | ||||
|     const soapRequest = `<?xml version="1.0" encoding="utf-8"?>
 | ||||
| <soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope"> | ||||
|   <soap12:Body> | ||||
|     <UpdateEmployeeEx xmlns="http://tempuri.org/"> | ||||
|       <UserName>${escapeXml(username)}</UserName> | ||||
|       <Password>${escapeXml(password)}</Password> | ||||
|       <EmployeeCode>${escapeXml(request.employeeCode)}</EmployeeCode> | ||||
|       <EmployeeName>${escapeXml(request.employeeName)}</EmployeeName> | ||||
|       <EmployeeLocation>${escapeXml(request.employeeLocation)}</EmployeeLocation> | ||||
|       <EmployeeRole>${escapeXml(request.employeeRole)}</EmployeeRole> | ||||
|       <EmployeeVerificationType>Card</EmployeeVerificationType> | ||||
|       <EmployeeExpiryFrom>${escapeXml(request.employeeExpiryFrom)}</EmployeeExpiryFrom> | ||||
|       <EmployeeExpiryTo>${escapeXml(request.employeeExpiryTo)}</EmployeeExpiryTo> | ||||
|       <EmployeeCardNumber>${escapeXml(request.employeeCardNumber)}</EmployeeCardNumber> | ||||
|       <GroupId></GroupId> | ||||
|       <EmployeePhoto></EmployeePhoto> | ||||
|     </UpdateEmployeeEx> | ||||
|   </soap12:Body> | ||||
| </soap12:Envelope> | ||||
| `;
 | ||||
|     return soapRequest; | ||||
| } | ||||
| 
 | ||||
| function parseUpdateEmployeeExResponse(soapResponse: string): string | null { | ||||
|     const parser = new DOMParser(); | ||||
|     const xmlDoc = parser.parseFromString(soapResponse, "text/xml"); | ||||
|     const currentElement = xmlDoc.documentElement.firstChild as HTMLElement; | ||||
|     const resultText = currentElement.textContent; | ||||
|     return resultText; | ||||
| } | ||||
| 
 | ||||
| function createDeleteEmployeeRequest(username: string, password: string, employeeCode: string): string | null { | ||||
|     if (!username || !password || !employeeCode) { | ||||
|         throw new Error('Missing required fields'); | ||||
|     } | ||||
|     const soapRequst = `<?xml version="1.0" encoding="utf-8"?>
 | ||||
| <soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope"> | ||||
|   <soap12:Body> | ||||
|     <DeleteEmployee xmlns="http://tempuri.org/"> | ||||
|       <UserName>${escapeXml(username)}</UserName> | ||||
|       <Password>${escapeXml(password)}</Password> | ||||
|       <EmployeeCode>${escapeXml(employeeCode)}</EmployeeCode> | ||||
|     </DeleteEmployee> | ||||
|   </soap12:Body> | ||||
| </soap12:Envelope>`; | ||||
|     return soapRequst; | ||||
| } | ||||
| 
 | ||||
| function parseDeleteEmployeeResponse(soapResponse: string): string | null { | ||||
|     const parser = new DOMParser(); | ||||
|     const xmlDoc = parser.parseFromString(soapResponse, "text/xml"); | ||||
|     const currentElement = xmlDoc.documentElement.firstChild as HTMLElement; | ||||
|     const resultText = currentElement.textContent; | ||||
|     return resultText; | ||||
| } | ||||
| 
 | ||||
| function createGetEmployeePunchLogsRequest(username: string, password: string, | ||||
|     employeeCode: string, attendanceDate: string): string | null { | ||||
|     const soapRequest = `<?xml version="1.0" encoding="utf-8"?>
 | ||||
| <soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope"> | ||||
|   <soap12:Body> | ||||
|     <GetEmployeePunchLogs xmlns="http://tempuri.org/"> | ||||
|       <UserName>cosqclient</UserName> | ||||
|       <Password>3bbb58d5</Password> | ||||
|       <EmployeeCode>1</EmployeeCode> | ||||
|       <AttendanceDate>2025-05-24</AttendanceDate> | ||||
|     </GetEmployeePunchLogs> | ||||
|   </soap12:Body> | ||||
| </soap12:Envelope>`; | ||||
|     return soapRequest; | ||||
| } | ||||
| 
 | ||||
| function createDateFromTime(date: Date, timeString: string): Date { | ||||
|     const [hour, minute, second] = timeString.split(':').map(str => parseInt(str, 10)); | ||||
|     const newDate = new Date(date.getTime()); | ||||
|     newDate.setHours(hour); | ||||
|     newDate.setMinutes(minute); | ||||
|     newDate.setSeconds(second); | ||||
|     return newDate; | ||||
| } | ||||
| 
 | ||||
| function parseGetEmployeePunchLogsResponse(soapResponse: string, attendanceDate: string): Date[] { | ||||
|     const rootDate = new Date(attendanceDate); | ||||
|     const parser = new DOMParser(); | ||||
|     const xmlDoc = parser.parseFromString(soapResponse, "text/xml"); | ||||
|     const currentElement = xmlDoc.documentElement.firstChild as HTMLElement; | ||||
|     const resultText = currentElement.textContent; | ||||
|     const punchLogs: Date[] = []; | ||||
|     const parts = resultText!.split(';'); | ||||
|     for (const part of parts) { | ||||
|         try { | ||||
|             const logDateTime = new Date(part); | ||||
|             if (isNaN(logDateTime.getTime())) { | ||||
|                 throw new Error('Invalid date format'); | ||||
|             } | ||||
|             punchLogs.push(logDateTime); | ||||
|         } catch { | ||||
|             try { | ||||
|                 const timeParts = part.split(','); | ||||
|                 for (const timePart of timeParts) { | ||||
|                     try { | ||||
|                         const logDateTime = createDateFromTime(rootDate, timePart); | ||||
|                         punchLogs.push(logDateTime); | ||||
|                     } catch { | ||||
|                         continue; | ||||
|                     } | ||||
|                 } | ||||
|             } catch { | ||||
|                 continue; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     const sortedLogs = punchLogs.sort((a, b) => b.getTime() - a.getTime()); | ||||
|     return sortedLogs; | ||||
| } | ||||
| 
 | ||||
| async function sendSoapRequest(soapRequest: string, endpoint: string) { | ||||
|     try { | ||||
|         const headers: any = { | ||||
|             'Content-Type': 'application/soap+xml; charset=utf-8', | ||||
|             'Content-Length': soapRequest.length.toString() | ||||
|         }; | ||||
| 
 | ||||
|         const response = await fetch(endpoint, { | ||||
|             method: 'POST', | ||||
|             headers: headers, | ||||
|             body: soapRequest | ||||
|         }); | ||||
| 
 | ||||
|         if (!response.ok) { | ||||
|             throw new Error(`HTTP error! status: ${response.status}`); | ||||
|         } | ||||
| 
 | ||||
|         return await response.text(); | ||||
|     } catch (error: any) { | ||||
|         throw new Error(`SOAP request failed: ${error.message}`); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| async function getUserDetails(username: string, | ||||
|     password: string, | ||||
|     employeeCode: string, endpoint: string): Promise<DoorAccessUser> { | ||||
|     const soapRequest = createGetEmployeeDetailsRequest(username, password, employeeCode); | ||||
|     const soapResponse = await sendSoapRequest(soapRequest!, endpoint); | ||||
|     const parsedResponse = parseGetEmployeeDetailsResponse(soapResponse); | ||||
|     return parsedResponse; | ||||
| } | ||||
| 
 | ||||
| async function updateUserEx(username: string, | ||||
|     password: string, | ||||
|     request: UpdateEmployeeExRequest, | ||||
|     endpoint: string) { | ||||
|     const soapRequest = createUpdateEmployeeExRequest(username, password, request); | ||||
|     const soapResponse = await sendSoapRequest(soapRequest!, endpoint); | ||||
|     const parsedResponse = parseUpdateEmployeeExResponse(soapResponse); | ||||
|     return parsedResponse; | ||||
| } | ||||
| 
 | ||||
| async function deleteEmplyee(username: string, | ||||
|     password: string, | ||||
|     employeeCode: string, endpoint: string) { | ||||
|     const soapRequest = createDeleteEmployeeRequest(username, password, employeeCode); | ||||
|     const soapResponse = await sendSoapRequest(soapRequest!, endpoint); | ||||
|     const parsedResponse = parseDeleteEmployeeResponse(soapResponse); | ||||
|     return parsedResponse; | ||||
| } | ||||
| 
 | ||||
| async function getEmployeePunchLogs(username: string, | ||||
|     password: string, | ||||
|     employeeCode: string, | ||||
|     attendanceDate: string, endpoint: string): Promise<Date[]> { | ||||
|     const soapRequest = createGetEmployeePunchLogsRequest(username, password, employeeCode, attendanceDate); | ||||
|     const soapResponse = await sendSoapRequest(soapRequest!, endpoint); | ||||
|     const parsedResponse = parseGetEmployeePunchLogsResponse(soapResponse, attendanceDate); | ||||
|     return parsedResponse; | ||||
| } | ||||
| 
 | ||||
| export const esslGetUserDetails = onRequest({ | ||||
|     region: '#{SERVICES_RGN}#' | ||||
| }, async (request: Request, response: Response) => { | ||||
|     return corsHandler(request, response, async () => { | ||||
|         try { | ||||
|             let username: string | null = request.body.username as string; | ||||
|             let password: string | null = request.body.password as string; | ||||
|             let endpoint: string | null = request.body.endpoint as string; | ||||
|             let gymId: string | null = request.body.gymId as string; | ||||
|             const getEmployeeDetailsRequest = request.body.params as EmployeeCodeRequest; | ||||
| 
 | ||||
|             if (!username) { | ||||
|                 throw new Error('Missing username or password'); | ||||
|             } | ||||
|             username = username.trim(); | ||||
|             if (!password) { | ||||
|                 if (!gymId) { | ||||
|                     throw new Error('Missing password or gymId'); | ||||
|                 } | ||||
|                 // todo: Get password from gym configuration by decrypting with private key
 | ||||
|                 throw new Error('Gym-based password retrieval not implemented yet'); | ||||
|             } | ||||
|             password = getDecryptedPassword(password); | ||||
|             if (!getEmployeeDetailsRequest) { | ||||
|                 throw new Error('Missing request params'); | ||||
|             } | ||||
|             const employeeCode = getEmployeeDetailsRequest.employeeCode; | ||||
|             if (!employeeCode) { | ||||
|                 throw new Error('Missing employeeCode'); | ||||
|             } | ||||
|             if (!endpoint) { | ||||
|                 throw new Error('Missing endpoint'); | ||||
|             } | ||||
|             if (!endpoint || endpoint.trim() === '') { | ||||
|                 throw new Error('Missing endpoint'); | ||||
|             } | ||||
|             try { | ||||
|                 new URL(endpoint); | ||||
|             } catch (_) { | ||||
|                 throw new Error('Endpoint is not a valid URI or URL'); | ||||
|             } | ||||
|             if (!endpoint.endsWith('/webservice.asmx')) { | ||||
|                 if (endpoint.endsWith('/')) { | ||||
|                     endpoint = endpoint.substring(0, endpoint.length - 1); | ||||
|                 } | ||||
|                 endpoint += '/webservice.asmx'; | ||||
|             } | ||||
|             const userDetails = await getUserDetails(username, password, employeeCode, endpoint); | ||||
|             response.send(userDetails); | ||||
|         } catch (error: any) { | ||||
|             logger.error(error); | ||||
|             response.status(500).send({ error: error.message }); | ||||
|         } | ||||
|     }) | ||||
| }); | ||||
| 
 | ||||
| export const esslUpdateUser = onRequest({ | ||||
|     region: '#{SERVICES_RGN}#' | ||||
| }, async (request: Request, response: Response) => { | ||||
|     return corsHandler(request, response, async () => { | ||||
|         try { | ||||
|             let username: string | null = request.body.username as string; | ||||
|             let password: string | null = request.body.password as string; | ||||
|             let endpoint: string | null = request.body.endpoint as string; | ||||
|             let gymId: string | null = request.body.gymId as string; | ||||
|             const updateEmployeeExRequest = request.body.params as UpdateEmployeeExRequest; | ||||
| 
 | ||||
|             if (!username) { | ||||
|                 throw new Error('Missing username'); | ||||
|             } | ||||
|             username = username.trim(); | ||||
| 
 | ||||
|             if (!password) { | ||||
|                 if (!gymId) { | ||||
|                     throw new Error('Missing password or gymId'); | ||||
|                 } | ||||
|                 // TODO: Get password from gym configuration by decrypting with private key
 | ||||
|                 throw new Error('Gym-based password retrieval not implemented yet'); | ||||
|             } | ||||
|             password = getDecryptedPassword(password); | ||||
|             if (!endpoint) { | ||||
|                 throw new Error('Missing endpoint'); | ||||
|             } | ||||
|             endpoint = endpoint.trim(); | ||||
|             if (!endpoint || endpoint.trim() === '') { | ||||
|                 throw new Error('Missing endpoint'); | ||||
|             } | ||||
|             try { | ||||
|                 new URL(endpoint); | ||||
|             } catch (_) { | ||||
|                 throw new Error('Endpoint is not a valid URI or URL'); | ||||
|             } | ||||
|             if (!endpoint.endsWith('/webservice.asmx')) { | ||||
|                 if (endpoint.endsWith('/')) { | ||||
|                     endpoint = endpoint.substring(0, endpoint.length - 1); | ||||
|                 } | ||||
|                 endpoint += '/webservice.asmx'; | ||||
|             } | ||||
|             if (!updateEmployeeExRequest) { | ||||
|                 throw new Error('Missing request params'); | ||||
|             } | ||||
|             const result = await updateUserEx(username, password, updateEmployeeExRequest, endpoint); | ||||
|             response.send(result); | ||||
|         } catch (error: any) { | ||||
|             logger.error(error); | ||||
|             response.status(500).send({ error: error.message }); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| export const esslDeleteUser = onRequest({ | ||||
|     region: '#{SERVICES_RGN}#' | ||||
| }, async (request: Request, response: Response) => { | ||||
|     return corsHandler(request, response, async () => { | ||||
|         try { | ||||
|             let username: string | null = request.body.username as string; | ||||
|             let password: string | null = request.body.password as string; | ||||
|             let endpoint: string | null = request.body.endpoint as string; | ||||
|             let gymId: string | null = request.body.gymId as string; | ||||
|             const getEmployeeDetailsRequest = request.body.params as EmployeeCodeRequest; | ||||
| 
 | ||||
|             if (!username) { | ||||
|                 throw new Error('Missing username'); | ||||
|             } | ||||
|             username = username.trim(); | ||||
| 
 | ||||
|             if (!password) { | ||||
|                 if (!gymId) { | ||||
|                     throw new Error('Missing password or gymId'); | ||||
|                 } | ||||
|                 // TODO: Get password from gym configuration by decrypting with private key
 | ||||
|                 throw new Error('Gym-based password retrieval not implemented yet'); | ||||
|             } | ||||
|             password = getDecryptedPassword(password); | ||||
|             if (!getEmployeeDetailsRequest) { | ||||
|                 throw new Error('Missing request params'); | ||||
|             } | ||||
|             const employeeCode = getEmployeeDetailsRequest.employeeCode; | ||||
|             if (!employeeCode) { | ||||
|                 throw new Error('Missing employeeCode'); | ||||
|             } | ||||
|             if (!endpoint) { | ||||
|                 throw new Error('Missing endpoint'); | ||||
|             } | ||||
|             if (!endpoint || endpoint.trim() === '') { | ||||
|                 throw new Error('Missing endpoint'); | ||||
|             } | ||||
|             try { | ||||
|                 new URL(endpoint); | ||||
|             } catch (_) { | ||||
|                 throw new Error('Endpoint is not a valid URI or URL'); | ||||
|             } | ||||
|             if (!endpoint.endsWith('/webservice.asmx')) { | ||||
|                 if (endpoint.endsWith('/')) { | ||||
|                     endpoint = endpoint.substring(0, endpoint.length - 1); | ||||
|                 } | ||||
|                 endpoint += '/webservice.asmx'; | ||||
|             } | ||||
|             const result = await deleteEmplyee(username, password, employeeCode, endpoint); | ||||
|             response.send(result); | ||||
|         } catch (error: any) { | ||||
|             logger.error(error); | ||||
|             response.status(500).send({ error: error.message }); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| 
 | ||||
| export const esslGetEmployeePunchLogs = onRequest({ | ||||
|     region: '#{SERVICES_RGN}#' | ||||
| }, async (request: Request, response: Response) => { | ||||
|     return corsHandler(request, response, async () => { | ||||
|         try { | ||||
|             let username: string | null = request.body.username as string; | ||||
|             let password: string | null = request.body.password as string; | ||||
|             let endpoint: string | null = request.body.endpoint as string; | ||||
|             let gymId: string | null = request.body.gymId as string; | ||||
|             const pushLogRequst = request.body.params as PushLogRequest; | ||||
| 
 | ||||
|             if (!username) { | ||||
|                 throw new Error('Missing username'); | ||||
|             } | ||||
|             username = username.trim(); | ||||
| 
 | ||||
|             if (!password) { | ||||
|                 if (!gymId) { | ||||
|                     throw new Error('Missing password or gymId'); | ||||
|                 } | ||||
|                 // TODO: Get password from gym configuration by decrypting with private key
 | ||||
|                 throw new Error('Gym-based password retrieval not implemented yet'); | ||||
|             } | ||||
|             password = getDecryptedPassword(password); | ||||
|             if (!pushLogRequst) { | ||||
|                 throw new Error('Missing request params'); | ||||
|             } | ||||
|             const employeeCode = pushLogRequst.employeeCode; | ||||
|             if (!employeeCode) { | ||||
|                 throw new Error('Missing employeeCode'); | ||||
|             } | ||||
|             const attendanceDate = pushLogRequst.attendanceDate; | ||||
|             if (!attendanceDate) { | ||||
|                 throw new Error('Missing attendanceDate'); | ||||
|             } | ||||
|             const isValidDate = /^\d{4}-\d{2}-\d{2}$/; | ||||
|             if (!attendanceDate.match(isValidDate)) { | ||||
|                 throw new Error('attendanceDate is not in the valid format YYYY-MM-DD'); | ||||
|             } | ||||
|             if (!endpoint) { | ||||
|                 throw new Error('Missing endpoint'); | ||||
|             } | ||||
|             if (!endpoint || endpoint.trim() === '') { | ||||
|                 throw new Error('Missing endpoint'); | ||||
|             } | ||||
|             try { | ||||
|                 new URL(endpoint); | ||||
|             } catch (_) { | ||||
|                 throw new Error('Endpoint is not a valid URI or URL'); | ||||
|             } | ||||
|             if (!endpoint.endsWith('/webservice.asmx')) { | ||||
|                 if (endpoint.endsWith('/')) { | ||||
|                     endpoint = endpoint.substring(0, endpoint.length - 1); | ||||
|                 } | ||||
|                 endpoint += '/webservice.asmx'; | ||||
|             } | ||||
|             const result = await getEmployeePunchLogs(username, | ||||
|                 password, | ||||
|                 employeeCode, | ||||
|                 attendanceDate, | ||||
|                 endpoint); | ||||
|             response.send(result); | ||||
|         } catch (error: any) { | ||||
|             logger.error(error); | ||||
|             response.status(500).send({ error: error.message }); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										4
									
								
								functions/src/dooraccess/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								functions/src/dooraccess/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| export { | ||||
|     esslGetUserDetails, esslUpdateUser, | ||||
|     esslDeleteUser, esslGetEmployeePunchLogs | ||||
| } from './essl'; | ||||
| @ -2,6 +2,7 @@ import { getLogger } from "../shared/config"; | ||||
| import { getCorsHandler } from "../shared/middleware"; | ||||
| import { onRequest } from "firebase-functions/v2/https"; | ||||
| import { Request } from "firebase-functions/v2/https"; | ||||
| import { Response } from "express"; | ||||
| import { SESClient } from "@aws-sdk/client-ses"; | ||||
| import { SendEmailCommand, SendRawEmailCommand } from "@aws-sdk/client-ses"; | ||||
| import { HttpsError } from "firebase-functions/v2/https"; | ||||
| @ -62,7 +63,7 @@ async function sendSimpleEmail(data: EmailRequest, recipients: string[]) { | ||||
| 
 | ||||
| async function sendEmailWithAttachments(data: EmailRequest, recipients: string[]) { | ||||
|     const ses = new SESClient({ | ||||
|         region: 'ap-south-1', | ||||
|         region: '#{SERVICES_RGN}#', | ||||
|         credentials: { | ||||
|             accessKeyId: process.env.AWS_ACCESS_KEY_ID || '', | ||||
|             secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '' | ||||
| @ -135,8 +136,8 @@ async function downloadFileFromUrl(url: string): Promise<Buffer> { | ||||
| } | ||||
| 
 | ||||
| export const sendEmailSES = onRequest({ | ||||
|     region: 'asia-south1' | ||||
| }, (request: Request, response) => { | ||||
|     region: '#{SERVICES_RGN}#' | ||||
| }, (request: Request, response: Response) => { | ||||
|     return corsHandler(request, response, async () => { | ||||
|         try { | ||||
|             const toAddress = request.body.toAddress; | ||||
|  | ||||
| @ -16,4 +16,8 @@ export { accessFile } from './storage'; | ||||
| export { processNotificationOnCreate } from './notifications'; | ||||
| export * from './payments'; | ||||
| export { getPlaceDetails, getPlacesAutocomplete } from './places'; | ||||
| export { registerClient } from './clientRegistration'; | ||||
| export { registerClient } from './users'; | ||||
| export { | ||||
|   esslGetUserDetails, esslUpdateUser, | ||||
|   esslDeleteUser, esslGetEmployeePunchLogs | ||||
| } from './dooraccess'; | ||||
|  | ||||
							
								
								
									
										51
									
								
								functions/src/shared/decrypt.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								functions/src/shared/decrypt.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | ||||
| import * as crypto from 'crypto'; | ||||
| import * as fs from 'fs'; | ||||
| import * as path from 'path'; | ||||
| 
 | ||||
| export class RSADecryption { | ||||
|     private static privateKeyObject: crypto.KeyObject | null = null; | ||||
| 
 | ||||
|     private static getPrivateKeyObject(): crypto.KeyObject { | ||||
|         if (!this.privateKeyObject) { | ||||
|             const keyPath = path.join(__dirname, '../../assets/keys/fitLien_private.pem'); | ||||
|             const keyContent = fs.readFileSync(keyPath, 'utf8'); | ||||
|             this.privateKeyObject = crypto.createPrivateKey({ | ||||
|                 key: keyContent, | ||||
|                 format: 'pem' | ||||
|             }); | ||||
|         } | ||||
|         return this.privateKeyObject; | ||||
|     } | ||||
| 
 | ||||
|     public static decryptPassword(encryptedPassword: string): string { | ||||
|         try { | ||||
|             if (!encryptedPassword || encryptedPassword.trim() === '') { | ||||
|                 throw new Error('Encrypted password cannot be empty'); | ||||
|             } | ||||
| 
 | ||||
|             const privateKeyObject = this.getPrivateKeyObject(); | ||||
|             const encryptedBuffer = Buffer.from(encryptedPassword, 'base64'); | ||||
| 
 | ||||
|             if (encryptedBuffer.length === 0) { | ||||
|                 throw new Error('Encrypted password buffer is empty'); | ||||
|             } | ||||
| 
 | ||||
|             const decryptedBuffer = crypto.privateDecrypt( | ||||
|                 { | ||||
|                     key: privateKeyObject, | ||||
|                     padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, | ||||
|                     oaepHash: 'sha1' | ||||
|                 }, | ||||
|                 encryptedBuffer | ||||
|             ); | ||||
| 
 | ||||
|             return decryptedBuffer.toString('utf8'); | ||||
|         } catch (error) { | ||||
|             console.error('Decryption error details:', { | ||||
|                 message: error instanceof Error ? error.message : 'Unknown error', | ||||
|                 encryptedPasswordLength: encryptedPassword?.length || 0 | ||||
|             }); | ||||
|             throw new Error(`Failed to decrypt password: ${error instanceof Error ? error.message : 'Unknown error'}`); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,6 +1,9 @@ | ||||
| import { onRequest } from "firebase-functions/v2/https"; | ||||
| import { getCorsHandler } from "../shared/middleware"; | ||||
| import { getAdmin, getLogger } from "../shared/config"; | ||||
| import { Request } from "firebase-functions/v2/https"; | ||||
| import { Response } from "express"; | ||||
| 
 | ||||
| 
 | ||||
| const corsHandler = getCorsHandler(); | ||||
| const admin = getAdmin(); | ||||
| @ -8,7 +11,7 @@ const logger = getLogger(); | ||||
| 
 | ||||
| export const registerClient = onRequest({ | ||||
|     region: '#{SERVICES_RGN}#' | ||||
| }, async (req, res) => { | ||||
| }, async (req: Request, res: Response) => { | ||||
|     return corsHandler(req, res, async () => { | ||||
|         try { | ||||
|             if (req.method !== 'POST') { | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user