From ee27b1b3cc3ae6c0c5ed25d85c0993e42cee2341 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Sun, 1 Jun 2025 09:36:02 +0530 Subject: [PATCH 01/13] Adding missing type inferences --- .../clientRegistration/clientRegistration.ts | 5 +++- functions/src/email/sendEmailSES.ts | 23 ++++++++++--------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/functions/src/clientRegistration/clientRegistration.ts b/functions/src/clientRegistration/clientRegistration.ts index fe2fafc..a168200 100644 --- a/functions/src/clientRegistration/clientRegistration.ts +++ b/functions/src/clientRegistration/clientRegistration.ts @@ -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') { diff --git a/functions/src/email/sendEmailSES.ts b/functions/src/email/sendEmailSES.ts index 78e6875..67b2886 100644 --- a/functions/src/email/sendEmailSES.ts +++ b/functions/src/email/sendEmailSES.ts @@ -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"; @@ -19,8 +20,8 @@ interface EmailRequest { from: string; replyTo?: string; attachments?: Attachment[]; - fileUrl?: string; - fileName?: string; + fileUrl?: string; + fileName?: string; } interface Attachment { @@ -136,13 +137,13 @@ async function downloadFileFromUrl(url: string): Promise { export const sendEmailSES = onRequest({ region: 'asia-south1' -}, (request: Request, response) => { +}, (request: Request, response: Response) => { return corsHandler(request, response, async () => { try { const toAddress = request.body.toAddress; const subject = request.body.subject; const message = request.body.message; - + // Initialize data with basic fields const data: EmailRequest = { to: toAddress, @@ -153,42 +154,42 @@ export const sendEmailSES = onRequest({ replyTo: process.env.SES_REPLY_TO_EMAIL || 'support@fitlien.com', attachments: request.body.attachments as Attachment[] || [] }; - + // Handle file URL if provided if (request.body.fileUrl && request.body.fileName) { logger.info(`Downloading attachment from URL: ${request.body.fileUrl}`); try { const fileContent = await downloadFileFromUrl(request.body.fileUrl); - + // If attachments array doesn't exist, create it if (!data.attachments) { data.attachments = []; } - + // Add the downloaded file as an attachment data.attachments.push({ filename: request.body.fileName, content: fileContent, contentType: mime.lookup(request.body.fileName) || 'application/octet-stream' }); - + logger.info(`Successfully downloaded attachment: ${request.body.fileName}`); } catch (downloadError) { logger.error(`Failed to download attachment: ${downloadError}`); throw new Error(`Failed to process attachment: ${downloadError}`); } } - + if (!data.to || !data.subject || !data.html || !data.from) { throw new HttpsError( 'invalid-argument', 'Missing required email fields' ); } - + logger.info(`Sending Email '${data.subject}' to '${data.to}' from '${data.from}'`); const recipients = Array.isArray(data.to) ? data.to : [data.to]; - + if (data.attachments && data.attachments.length > 0) { const messageResult = await sendEmailWithAttachments(data, recipients); response.status(200).json(messageResult); From e5173679673c67cbc033af890b2d1fc29a89786a Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Sun, 1 Jun 2025 09:38:57 +0530 Subject: [PATCH 02/13] Renamed modules --- functions/src/index.ts | 2 +- .../src/{clientRegistration => users}/clientRegistration.ts | 0 functions/src/{clientRegistration => users}/index.ts | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename functions/src/{clientRegistration => users}/clientRegistration.ts (100%) rename functions/src/{clientRegistration => users}/index.ts (100%) diff --git a/functions/src/index.ts b/functions/src/index.ts index 432a73c..314a462 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -16,4 +16,4 @@ export { accessFile } from './storage'; export { processNotificationOnCreate } from './notifications'; export * from './payments'; export { getPlaceDetails, getPlacesAutocomplete } from './places'; -export { registerClient } from './clientRegistration'; +export { registerClient } from './users'; diff --git a/functions/src/clientRegistration/clientRegistration.ts b/functions/src/users/clientRegistration.ts similarity index 100% rename from functions/src/clientRegistration/clientRegistration.ts rename to functions/src/users/clientRegistration.ts diff --git a/functions/src/clientRegistration/index.ts b/functions/src/users/index.ts similarity index 100% rename from functions/src/clientRegistration/index.ts rename to functions/src/users/index.ts From 47bd8610d24bdebb2cd34f18304d47c61b36d017 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Mon, 9 Jun 2025 10:03:16 +0530 Subject: [PATCH 03/13] Adding esslGetUserDetails --- functions/package-lock.json | 18 ++- functions/package.json | 4 +- functions/src/dooraccess/doorAccessUser.ts | 7 + functions/src/dooraccess/essl.ts | 162 +++++++++++++++++++++ functions/src/dooraccess/index.ts | 1 + functions/src/email/sendEmailSES.ts | 2 +- functions/src/index.ts | 1 + 7 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 functions/src/dooraccess/doorAccessUser.ts create mode 100644 functions/src/dooraccess/essl.ts create mode 100644 functions/src/dooraccess/index.ts diff --git a/functions/package-lock.json b/functions/package-lock.json index 853a3bf..4de688d 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -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/pdfmake": "^0.2.11", + "@types/xmldom": "^0.1.34", "firebase-functions-test": "^3.1.0", "typescript": "^5.8.2" }, @@ -2946,6 +2948,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 +8332,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", diff --git a/functions/package.json b/functions/package.json index b29002f..d95bb67 100644 --- a/functions/package.json +++ b/functions/package.json @@ -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/pdfmake": "^0.2.11", + "@types/xmldom": "^0.1.34", "firebase-functions-test": "^3.1.0", "typescript": "^5.8.2" }, diff --git a/functions/src/dooraccess/doorAccessUser.ts b/functions/src/dooraccess/doorAccessUser.ts new file mode 100644 index 0000000..58fcd2c --- /dev/null +++ b/functions/src/dooraccess/doorAccessUser.ts @@ -0,0 +1,7 @@ +export type DoorAccessUser = { + name: string; + location: string; + role: string; + expireFrom: Date | null; + expireTo: Date | null; +}; diff --git a/functions/src/dooraccess/essl.ts b/functions/src/dooraccess/essl.ts new file mode 100644 index 0000000..231b06a --- /dev/null +++ b/functions/src/dooraccess/essl.ts @@ -0,0 +1,162 @@ +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'; + +const logger = getLogger(); +const corsHandler = getCorsHandler(); + +const escapeXml = (str: string) => { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +}; + +function createGetEmployeeDetailsRequest(username: string, password: string, employeeCode: string) { + const soapRequest = ` + + + + ${escapeXml(username)} + ${escapeXml(password)} + ${escapeXml(employeeCode)} + + +`; + + 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; +} + +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) { + const soapRequest = createGetEmployeeDetailsRequest(username, password, employeeCode); + const soapResponse = await sendSoapRequest(soapRequest, endpoint); + const parsedResponse = parseGetEmployeeDetailsResponse(soapResponse); + return parsedResponse; +} + +export const esslGetUserDetails = onRequest({ + region: '#{SERVICES_RGN}#' +}, async (request: Request, response: Response) => { + return corsHandler(request, response, async () => { + try { + const username = request.body.username; + const password = request.body.password; + const employeeCode = request.body.employeeCode; + const endpoint = request.body.endpoint; + if ((!username) || (!password)) { + throw new Error('Missing username or password'); + } + if (!employeeCode) { + throw new Error('Missing employee code'); + } + if (!endpoint) { + throw new Error('Missing endpoint'); + } + const userDetails = await getUserDetails(username, password, employeeCode, endpoint); + response.send(userDetails); + } catch (error: any) { + logger.error(error); + response.status(500).send({ error: error.message }); + } + }) +}); diff --git a/functions/src/dooraccess/index.ts b/functions/src/dooraccess/index.ts new file mode 100644 index 0000000..a855858 --- /dev/null +++ b/functions/src/dooraccess/index.ts @@ -0,0 +1 @@ +export { esslGetUserDetails } from './essl'; \ No newline at end of file diff --git a/functions/src/email/sendEmailSES.ts b/functions/src/email/sendEmailSES.ts index 67b2886..8d0437c 100644 --- a/functions/src/email/sendEmailSES.ts +++ b/functions/src/email/sendEmailSES.ts @@ -63,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 || '' diff --git a/functions/src/index.ts b/functions/src/index.ts index 314a462..12cacad 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -17,3 +17,4 @@ export { processNotificationOnCreate } from './notifications'; export * from './payments'; export { getPlaceDetails, getPlacesAutocomplete } from './places'; export { registerClient } from './users'; +export { esslGetUserDetails } from './dooraccess'; From 09068fe731316a83b556bd4682bf264c599e09d3 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Mon, 9 Jun 2025 10:04:35 +0530 Subject: [PATCH 04/13] Update .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b17f631..2bedd41 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,6 @@ node_modules/ # dataconnect generated files .dataconnect + +.DS_Store +**/.DS_Store \ No newline at end of file From bb3e966dafdefb5d43f6035a4b3663a310d78d7c Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Mon, 9 Jun 2025 14:24:19 +0530 Subject: [PATCH 05/13] Adding updateEmployeeEx --- functions/src/dooraccess/essl.ts | 165 +++++++++++++++++++++++++++- functions/src/dooraccess/index.ts | 2 +- functions/src/email/sendEmailSES.ts | 2 +- functions/src/index.ts | 2 +- 4 files changed, 162 insertions(+), 9 deletions(-) diff --git a/functions/src/dooraccess/essl.ts b/functions/src/dooraccess/essl.ts index 231b06a..c45ea64 100644 --- a/functions/src/dooraccess/essl.ts +++ b/functions/src/dooraccess/essl.ts @@ -9,6 +9,23 @@ import { DOMParser } from 'xmldom'; const logger = getLogger(); const corsHandler = getCorsHandler(); +export interface GetEmployeeDetailsRquest { + employeeCode: string; +} + +export interface UpdateEmployeeExRequest { + employeeCode: string; + employeeName: string; + employeeLocation: string; + employeeRole: string; + employeeVerificationType: string; + employeeExpiryFrom: string; + employeeExpiryTo: string; + employeeCardNumber: string; + groupId: string; + employeePhoto: string; +} + const escapeXml = (str: string) => { return str .replace(/&/g, '&') @@ -102,6 +119,52 @@ function parseGetEmployeeDetailsResponse(soapResponse: string): DoorAccessUser { 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) { + + 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 = ` + + + + ${escapeXml(username)} + ${escapeXml(password)} + ${escapeXml(request.employeeCode)} + ${escapeXml(request.employeeName)} + ${escapeXml(request.employeeLocation)} + ${escapeXml(request.employeeRole)} + Card + ${escapeXml(request.employeeExpiryFrom)} + ${escapeXml(request.employeeExpiryTo)} + ${escapeXml(request.employeeCardNumber)} + + + + + +`; + 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; +} + async function sendSoapRequest(soapRequest: string, endpoint: string) { try { const headers: any = { @@ -134,24 +197,63 @@ async function getUserDetails(username: string, return parsedResponse; } +async function updateEmployeeEx(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; +} + export const esslGetUserDetails = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response: Response) => { return corsHandler(request, response, async () => { try { - const username = request.body.username; - const password = request.body.password; - const employeeCode = request.body.employeeCode; - const endpoint = request.body.endpoint; - if ((!username) || (!password)) { + 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 GetEmployeeDetailsRquest; + + 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 + } + password = password.trim(); + if (!getEmployeeDetailsRequest) { + throw new Error('Missing request params'); + } + const employeeCode = getEmployeeDetailsRequest.employeeCode; if (!employeeCode) { - throw new Error('Missing employee code'); + 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) { @@ -160,3 +262,54 @@ export const esslGetUserDetails = onRequest({ } }) }); + +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 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 + } + 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 updateEmployeeEx(username, password, updateEmployeeExRequest, endpoint); + response.send(result); + } catch (error: any) { + logger.error(error); + response.status(500).send({ error: error.message }); + } + }); +}); diff --git a/functions/src/dooraccess/index.ts b/functions/src/dooraccess/index.ts index a855858..497e0cb 100644 --- a/functions/src/dooraccess/index.ts +++ b/functions/src/dooraccess/index.ts @@ -1 +1 @@ -export { esslGetUserDetails } from './essl'; \ No newline at end of file +export { esslGetUserDetails, esslUpdateUser } from './essl'; \ No newline at end of file diff --git a/functions/src/email/sendEmailSES.ts b/functions/src/email/sendEmailSES.ts index 8d0437c..5efc40a 100644 --- a/functions/src/email/sendEmailSES.ts +++ b/functions/src/email/sendEmailSES.ts @@ -136,7 +136,7 @@ async function downloadFileFromUrl(url: string): Promise { } export const sendEmailSES = onRequest({ - region: 'asia-south1' + region: '#{SERVICES_RGN}#' }, (request: Request, response: Response) => { return corsHandler(request, response, async () => { try { diff --git a/functions/src/index.ts b/functions/src/index.ts index 12cacad..362d85d 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -17,4 +17,4 @@ export { processNotificationOnCreate } from './notifications'; export * from './payments'; export { getPlaceDetails, getPlacesAutocomplete } from './places'; export { registerClient } from './users'; -export { esslGetUserDetails } from './dooraccess'; +export { esslGetUserDetails, esslUpdateUser } from './dooraccess'; From 72b6bb3cd60888b2dcde185f230bf6086cbcc2e2 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Mon, 9 Jun 2025 15:08:00 +0530 Subject: [PATCH 06/13] Added esslDeleteEmployee --- functions/src/dooraccess/essl.ts | 97 +++++++++++++++++++++++++++++-- functions/src/dooraccess/index.ts | 2 +- functions/src/index.ts | 2 +- 3 files changed, 93 insertions(+), 8 deletions(-) diff --git a/functions/src/dooraccess/essl.ts b/functions/src/dooraccess/essl.ts index c45ea64..0c4e54c 100644 --- a/functions/src/dooraccess/essl.ts +++ b/functions/src/dooraccess/essl.ts @@ -9,7 +9,7 @@ import { DOMParser } from 'xmldom'; const logger = getLogger(); const corsHandler = getCorsHandler(); -export interface GetEmployeeDetailsRquest { +export interface EmployeeCodeRequest { employeeCode: string; } @@ -35,7 +35,7 @@ const escapeXml = (str: string) => { .replace(/'/g, '''); }; -function createGetEmployeeDetailsRequest(username: string, password: string, employeeCode: string) { +function createGetEmployeeDetailsRequest(username: string, password: string, employeeCode: string): string | null { const soapRequest = ` @@ -124,7 +124,7 @@ function isValidDateString(dateString: string): boolean { return dateRegex.test(dateString); } -function createUpdateEmployeeExRequest(username: string, password: string, request: UpdateEmployeeExRequest) { +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'); @@ -165,6 +165,31 @@ function parseUpdateEmployeeExResponse(soapResponse: string): string | null { return resultText; } +function createDeleteEmployeeRequest(username: string, password: string, employeeCode: string): string | null { + if (!username || !password || !employeeCode) { + throw new Error('Missing required fields'); + } + const soapRequst = ` + + + + ${escapeXml(username)} + ${escapeXml(password)} + ${escapeXml(employeeCode)} + + +`; + 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; +} + async function sendSoapRequest(soapRequest: string, endpoint: string) { try { const headers: any = { @@ -192,7 +217,7 @@ async function getUserDetails(username: string, password: string, employeeCode: string, endpoint: string) { const soapRequest = createGetEmployeeDetailsRequest(username, password, employeeCode); - const soapResponse = await sendSoapRequest(soapRequest, endpoint); + const soapResponse = await sendSoapRequest(soapRequest!, endpoint); const parsedResponse = parseGetEmployeeDetailsResponse(soapResponse); return parsedResponse; } @@ -202,11 +227,20 @@ async function updateEmployeeEx(username: string, request: UpdateEmployeeExRequest, endpoint: string) { const soapRequest = createUpdateEmployeeExRequest(username, password, request); - const soapResponse = await sendSoapRequest(soapRequest, endpoint); + 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; +} + export const esslGetUserDetails = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response: Response) => { @@ -217,7 +251,7 @@ export const esslGetUserDetails = onRequest({ let endpoint: string | null = request.body.endpoint as string; let gymId: string | null = request.body.gymId as string; - const getEmployeeDetailsRequest = request.body.params as GetEmployeeDetailsRquest; + const getEmployeeDetailsRequest = request.body.params as EmployeeCodeRequest; if (!username) { throw new Error('Missing username or password'); @@ -313,3 +347,54 @@ export const esslUpdateUser = onRequest({ } }); }); + +export const esslDeleteEmployee = onRequest({ + region: '#{SERVICES_RGN}#' +}, async (request: Request, response: Response) => { + return corsHandler(request, response, async () => { + 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 + } + password = password.trim(); + 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); + }); +}); diff --git a/functions/src/dooraccess/index.ts b/functions/src/dooraccess/index.ts index 497e0cb..41b07a8 100644 --- a/functions/src/dooraccess/index.ts +++ b/functions/src/dooraccess/index.ts @@ -1 +1 @@ -export { esslGetUserDetails, esslUpdateUser } from './essl'; \ No newline at end of file +export { esslGetUserDetails, esslUpdateUser, esslDeleteEmployee } from './essl'; \ No newline at end of file diff --git a/functions/src/index.ts b/functions/src/index.ts index 362d85d..975178c 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -17,4 +17,4 @@ export { processNotificationOnCreate } from './notifications'; export * from './payments'; export { getPlaceDetails, getPlacesAutocomplete } from './places'; export { registerClient } from './users'; -export { esslGetUserDetails, esslUpdateUser } from './dooraccess'; +export { esslGetUserDetails, esslUpdateUser, esslDeleteEmployee } from './dooraccess'; From cbe98e8cd0491e1714583a15df3055d29a898da4 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Mon, 9 Jun 2025 15:26:53 +0530 Subject: [PATCH 07/13] Replaced esslDeleteEmployee to esslDeleteUser --- functions/src/dooraccess/essl.ts | 2 +- functions/src/dooraccess/index.ts | 2 +- functions/src/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/functions/src/dooraccess/essl.ts b/functions/src/dooraccess/essl.ts index 0c4e54c..c70f687 100644 --- a/functions/src/dooraccess/essl.ts +++ b/functions/src/dooraccess/essl.ts @@ -348,7 +348,7 @@ export const esslUpdateUser = onRequest({ }); }); -export const esslDeleteEmployee = onRequest({ +export const esslDeleteUser = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response: Response) => { return corsHandler(request, response, async () => { diff --git a/functions/src/dooraccess/index.ts b/functions/src/dooraccess/index.ts index 41b07a8..587a1da 100644 --- a/functions/src/dooraccess/index.ts +++ b/functions/src/dooraccess/index.ts @@ -1 +1 @@ -export { esslGetUserDetails, esslUpdateUser, esslDeleteEmployee } from './essl'; \ No newline at end of file +export { esslGetUserDetails, esslUpdateUser, esslDeleteUser } from './essl'; \ No newline at end of file diff --git a/functions/src/index.ts b/functions/src/index.ts index 975178c..515cc3b 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -17,4 +17,4 @@ export { processNotificationOnCreate } from './notifications'; export * from './payments'; export { getPlaceDetails, getPlacesAutocomplete } from './places'; export { registerClient } from './users'; -export { esslGetUserDetails, esslUpdateUser, esslDeleteEmployee } from './dooraccess'; +export { esslGetUserDetails, esslUpdateUser, esslDeleteUser } from './dooraccess'; From 5e680c39470458b47fa07039e5b0d153f8574f91 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Mon, 9 Jun 2025 18:47:58 +0530 Subject: [PATCH 08/13] Adding createGetEmployeePunchLogsRequest --- functions/src/dooraccess/essl.ts | 143 +++++++++++++++++++++++++++++- functions/src/dooraccess/index.ts | 5 +- functions/src/index.ts | 5 +- 3 files changed, 148 insertions(+), 5 deletions(-) diff --git a/functions/src/dooraccess/essl.ts b/functions/src/dooraccess/essl.ts index c70f687..8be6a88 100644 --- a/functions/src/dooraccess/essl.ts +++ b/functions/src/dooraccess/essl.ts @@ -13,6 +13,10 @@ export interface EmployeeCodeRequest { employeeCode: string; } +export interface PushLogRequest extends EmployeeCodeRequest { + attendanceDate: string; +} + export interface UpdateEmployeeExRequest { employeeCode: string; employeeName: string; @@ -190,6 +194,66 @@ function parseDeleteEmployeeResponse(soapResponse: string): string | null { return resultText; } +function createGetEmployeePunchLogsRequest(username: string, password: string, + employeeCode: string, attendanceDate: string): string | null { + const soapRequest = ` + + + + cosqclient + 3bbb58d5 + 1 + 2025-05-24 + + +`; + 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 = { @@ -215,14 +279,14 @@ async function sendSoapRequest(soapRequest: string, endpoint: string) { async function getUserDetails(username: string, password: string, - employeeCode: string, endpoint: string) { + employeeCode: string, endpoint: string): Promise { const soapRequest = createGetEmployeeDetailsRequest(username, password, employeeCode); const soapResponse = await sendSoapRequest(soapRequest!, endpoint); const parsedResponse = parseGetEmployeeDetailsResponse(soapResponse); return parsedResponse; } -async function updateEmployeeEx(username: string, +async function updateUserEx(username: string, password: string, request: UpdateEmployeeExRequest, endpoint: string) { @@ -241,6 +305,16 @@ async function deleteEmplyee(username: string, return parsedResponse; } +async function getEmployeePunchLogs(username: string, + password: string, + employeeCode: string, + attendanceDate: string, endpoint: string): Promise { + 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) => { @@ -339,7 +413,7 @@ export const esslUpdateUser = onRequest({ if (!updateEmployeeExRequest) { throw new Error('Missing request params'); } - const result = await updateEmployeeEx(username, password, updateEmployeeExRequest, endpoint); + const result = await updateUserEx(username, password, updateEmployeeExRequest, endpoint); response.send(result); } catch (error: any) { logger.error(error); @@ -398,3 +472,66 @@ export const esslDeleteUser = onRequest({ response.send(result); }); }); + +export const esslGetEmployeePunchLogs = onRequest({ + region: '#{SERVICES_RGN}#' +}, async (request: Request, response: Response) => { + return corsHandler(request, response, async () => { + 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 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 + } + password = password.trim(); + 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); + }); +}); diff --git a/functions/src/dooraccess/index.ts b/functions/src/dooraccess/index.ts index 587a1da..ad7da6e 100644 --- a/functions/src/dooraccess/index.ts +++ b/functions/src/dooraccess/index.ts @@ -1 +1,4 @@ -export { esslGetUserDetails, esslUpdateUser, esslDeleteUser } from './essl'; \ No newline at end of file +export { + esslGetUserDetails, esslUpdateUser, + esslDeleteUser, esslGetEmployeePunchLogs +} from './essl'; \ No newline at end of file diff --git a/functions/src/index.ts b/functions/src/index.ts index 515cc3b..1156209 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -17,4 +17,7 @@ export { processNotificationOnCreate } from './notifications'; export * from './payments'; export { getPlaceDetails, getPlacesAutocomplete } from './places'; export { registerClient } from './users'; -export { esslGetUserDetails, esslUpdateUser, esslDeleteUser } from './dooraccess'; +export { + esslGetUserDetails, esslUpdateUser, + esslDeleteUser, esslGetEmployeePunchLogs +} from './dooraccess'; From 77d642eac152f6c13f5096d2d311aef72b8be152 Mon Sep 17 00:00:00 2001 From: DhanshCOSQ Date: Wed, 11 Jun 2025 08:12:30 +0000 Subject: [PATCH 09/13] password complete (#57) Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/57 Co-authored-by: DhanshCOSQ Co-committed-by: DhanshCOSQ --- .gitignore | 3 + functions/package-lock.json | 9 +- functions/package.json | 2 +- functions/src/dooraccess/essl.ts | 225 +++++++++++++++++-------------- functions/src/shared/decrypt.ts | 51 +++++++ 5 files changed, 184 insertions(+), 106 deletions(-) create mode 100644 functions/src/shared/decrypt.ts diff --git a/.gitignore b/.gitignore index 2bedd41..b848764 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/functions/package-lock.json b/functions/package-lock.json index 4de688d..b349a53 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -31,7 +31,7 @@ "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", @@ -2831,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" } diff --git a/functions/package.json b/functions/package.json index d95bb67..334660d 100644 --- a/functions/package.json +++ b/functions/package.json @@ -38,7 +38,7 @@ "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", diff --git a/functions/src/dooraccess/essl.ts b/functions/src/dooraccess/essl.ts index 8be6a88..ff26ad3 100644 --- a/functions/src/dooraccess/essl.ts +++ b/functions/src/dooraccess/essl.ts @@ -5,7 +5,7 @@ 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(); @@ -30,6 +30,13 @@ export interface UpdateEmployeeExRequest { 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, '&') @@ -324,7 +331,6 @@ export const esslGetUserDetails = onRequest({ 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) { @@ -336,8 +342,9 @@ export const esslGetUserDetails = onRequest({ 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 = password.trim(); + password = getDecryptedPassword(password); if (!getEmployeeDetailsRequest) { throw new Error('Missing request params'); } @@ -380,18 +387,21 @@ export const esslUpdateUser = onRequest({ 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 or password'); + 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 + // 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'); } @@ -426,112 +436,125 @@ export const esslDeleteUser = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response: Response) => { return corsHandler(request, response, async () => { - 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 - } - password = password.trim(); - 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); + 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'); } - endpoint += '/webservice.asmx'; + 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 }); } - const result = await deleteEmplyee(username, password, employeeCode, endpoint); - response.send(result); }); }); + export const esslGetEmployeePunchLogs = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response: Response) => { return corsHandler(request, response, async () => { - 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 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 - } - password = password.trim(); - 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); + 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'); } - endpoint += '/webservice.asmx'; + 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 }); } - const result = await getEmployeePunchLogs(username, - password, - employeeCode, - attendanceDate, - endpoint); - response.send(result); }); -}); +}); \ No newline at end of file diff --git a/functions/src/shared/decrypt.ts b/functions/src/shared/decrypt.ts new file mode 100644 index 0000000..63e9b9f --- /dev/null +++ b/functions/src/shared/decrypt.ts @@ -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'}`); + } + } +} From 3e1a51c093f896355e198e6a240b37d15aa4bffd Mon Sep 17 00:00:00 2001 From: DhanshCOSQ Date: Wed, 11 Jun 2025 09:45:33 +0000 Subject: [PATCH 10/13] feature/essl-password (#58) Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/58 Co-authored-by: DhanshCOSQ Co-committed-by: DhanshCOSQ --- .gitea/workflows/deploy-dev.yaml | 6 ++++++ .gitea/workflows/deploy-qa.yaml | 6 ++++++ .gitea/workflows/deploy.yaml | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index f0f0a82..cbb62b4 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -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 diff --git a/.gitea/workflows/deploy-qa.yaml b/.gitea/workflows/deploy-qa.yaml index c868dcb..e57c4bc 100644 --- a/.gitea/workflows/deploy-qa.yaml +++ b/.gitea/workflows/deploy-qa.yaml @@ -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 diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index dbc0dd5..8ce1e58 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -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 From e27e189f7ec49304e6f0385d5f531421588845e2 Mon Sep 17 00:00:00 2001 From: Sharon Dcruz Date: Thu, 12 Jun 2025 08:25:49 +0000 Subject: [PATCH 11/13] phone number updated (#59) Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/59 Co-authored-by: Sharon Dcruz Co-committed-by: Sharon Dcruz --- functions/src/users/clientRegistration.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/functions/src/users/clientRegistration.ts b/functions/src/users/clientRegistration.ts index a168200..89f4eab 100644 --- a/functions/src/users/clientRegistration.ts +++ b/functions/src/users/clientRegistration.ts @@ -82,7 +82,6 @@ export const registerClient = onRequest({ const clientData = { ...gymUser, - phoneNumber: formattedPhoneNumber, }; await admin.firestore().collection('client_profiles').doc(clientUid).set(clientData); From 03f69415311d261cf33dd10121a11dfa3944ff46 Mon Sep 17 00:00:00 2001 From: DhanshCOSQ Date: Thu, 12 Jun 2025 09:41:37 +0000 Subject: [PATCH 12/13] feature/essl-password (#60) Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/60 Co-authored-by: DhanshCOSQ Co-committed-by: DhanshCOSQ --- functions/src/users/clientRegistration.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/functions/src/users/clientRegistration.ts b/functions/src/users/clientRegistration.ts index 89f4eab..f39b827 100644 --- a/functions/src/users/clientRegistration.ts +++ b/functions/src/users/clientRegistration.ts @@ -36,27 +36,21 @@ export const registerClient = onRequest({ return res.status(403).json({ error: 'Forbidden. Only gym owners can register clients.' }); } const gymUser = req.body; - if (!gymUser.phoneNumber) { + if (!gymUser.fields["phone-number"]) { return res.status(400).json({ error: 'Phone number is required' }); } - const isdCode = gymUser.isdCode || '91'; - const formattedPhoneNumber = gymUser.phoneNumber.startsWith('+') - ? gymUser.phoneNumber - : `${isdCode}${gymUser.phoneNumber}`; - let clientUid; try { - const userRecord = await admin.auth().getUserByPhoneNumber(formattedPhoneNumber) + const userRecord = await admin.auth().getUserByPhoneNumber(gymUser.fields["phone-number"]) .catch(() => null); if (userRecord) { clientUid = userRecord.uid; } else { const newUser = await admin.auth().createUser({ - phoneNumber: formattedPhoneNumber, - displayName: gymUser.name || '', - email: gymUser.email || null, + phoneNumber: gymUser.fields["phone-number"], + displayName: gymUser.fields["first-name"] || '', }); clientUid = newUser.uid; } From b9346de5e5333e9d19842269ac55844e8f4a22f1 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Thu, 12 Jun 2025 19:44:35 +0530 Subject: [PATCH 13/13] Setting private key for prod --- .gitea/workflows/deploy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index 8ce1e58..d01f7eb 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -28,7 +28,7 @@ jobs: - name: Create private key file run: | mkdir -p functions/assets/keys - echo "${{ secrets.FITLIEN_PRIVATEKEY_DEV }}" > functions/assets/keys/fitLien_private.pem + echo "${{ secrets.FITLIEN_PRIVATEKEY }}" > functions/assets/keys/fitLien_private.pem chmod 600 functions/assets/keys/fitLien_private.pem - name: Replace variables in .env