password complete (#57)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m42s

Reviewed-on: #57
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
This commit is contained in:
Dhansh A S 2025-06-11 08:12:30 +00:00 committed by Dhansh A S
parent 5e680c3947
commit 77d642eac1
5 changed files with 184 additions and 106 deletions

3
.gitignore vendored
View File

@ -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

View File

@ -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"
}

View File

@ -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",

View File

@ -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, '&amp;')
@ -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);
});
});
});

View 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'}`);
}
}
}