fitlien-services/functions/src/dooraccess/essl.ts
DhanshCOSQ 77d642eac1
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m42s
password complete (#57)
Reviewed-on: #57
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2025-06-11 08:12:30 +00:00

560 lines
21 KiB
TypeScript

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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
};
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 });
}
});
});