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(); 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; } const escapeXml = (str: string) => { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }; function createGetEmployeeDetailsRequest(username: string, password: string, employeeCode: string): string | null { 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; } 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 = ` ${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; } 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; } 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 = { '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 { 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 { 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 } 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 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 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 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 () => { 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); }); }); 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); }); });