password complete (#57)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m42s
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:
parent
5e680c3947
commit
77d642eac1
3
.gitignore
vendored
3
.gitignore
vendored
@ -26,6 +26,9 @@ pids
|
|||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
lib-cov
|
lib-cov
|
||||||
|
|
||||||
|
# Private key
|
||||||
|
/functions/assets/keys/fitLien_private.pem
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
# Coverage directory used by tools like istanbul
|
||||||
coverage
|
coverage
|
||||||
|
|
||||||
|
|||||||
9
functions/package-lock.json
generated
9
functions/package-lock.json
generated
@ -31,7 +31,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/long": "^5.0.0",
|
"@types/long": "^5.0.0",
|
||||||
"@types/mime-types": "^2.1.4",
|
"@types/mime-types": "^2.1.4",
|
||||||
"@types/node": "^22.13.14",
|
"@types/node": "^22.15.31",
|
||||||
"@types/pdfmake": "^0.2.11",
|
"@types/pdfmake": "^0.2.11",
|
||||||
"@types/xmldom": "^0.1.34",
|
"@types/xmldom": "^0.1.34",
|
||||||
"firebase-functions-test": "^3.1.0",
|
"firebase-functions-test": "^3.1.0",
|
||||||
@ -2831,9 +2831,10 @@
|
|||||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="
|
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.15.2",
|
"version": "22.15.31",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.31.tgz",
|
||||||
"integrity": "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==",
|
"integrity": "sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/long": "^5.0.0",
|
"@types/long": "^5.0.0",
|
||||||
"@types/mime-types": "^2.1.4",
|
"@types/mime-types": "^2.1.4",
|
||||||
"@types/node": "^22.13.14",
|
"@types/node": "^22.15.31",
|
||||||
"@types/pdfmake": "^0.2.11",
|
"@types/pdfmake": "^0.2.11",
|
||||||
"@types/xmldom": "^0.1.34",
|
"@types/xmldom": "^0.1.34",
|
||||||
"firebase-functions-test": "^3.1.0",
|
"firebase-functions-test": "^3.1.0",
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { Response } from "express";
|
|||||||
import { getCorsHandler } from "../shared/middleware";
|
import { getCorsHandler } from "../shared/middleware";
|
||||||
import { getLogger } from "../shared/config";
|
import { getLogger } from "../shared/config";
|
||||||
import { DOMParser } from 'xmldom';
|
import { DOMParser } from 'xmldom';
|
||||||
|
import { RSADecryption } from "../shared/decrypt";
|
||||||
const logger = getLogger();
|
const logger = getLogger();
|
||||||
const corsHandler = getCorsHandler();
|
const corsHandler = getCorsHandler();
|
||||||
|
|
||||||
@ -30,6 +30,13 @@ export interface UpdateEmployeeExRequest {
|
|||||||
employeePhoto: 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) => {
|
const escapeXml = (str: string) => {
|
||||||
return str
|
return str
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
@ -324,7 +331,6 @@ export const esslGetUserDetails = onRequest({
|
|||||||
let password: string | null = request.body.password as string;
|
let password: string | null = request.body.password as string;
|
||||||
let endpoint: string | null = request.body.endpoint as string;
|
let endpoint: string | null = request.body.endpoint as string;
|
||||||
let gymId: string | null = request.body.gymId as string;
|
let gymId: string | null = request.body.gymId as string;
|
||||||
|
|
||||||
const getEmployeeDetailsRequest = request.body.params as EmployeeCodeRequest;
|
const getEmployeeDetailsRequest = request.body.params as EmployeeCodeRequest;
|
||||||
|
|
||||||
if (!username) {
|
if (!username) {
|
||||||
@ -336,8 +342,9 @@ export const esslGetUserDetails = onRequest({
|
|||||||
throw new Error('Missing password or 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 = password.trim();
|
password = getDecryptedPassword(password);
|
||||||
if (!getEmployeeDetailsRequest) {
|
if (!getEmployeeDetailsRequest) {
|
||||||
throw new Error('Missing request params');
|
throw new Error('Missing request params');
|
||||||
}
|
}
|
||||||
@ -380,18 +387,21 @@ export const esslUpdateUser = onRequest({
|
|||||||
let password: string | null = request.body.password as string;
|
let password: string | null = request.body.password as string;
|
||||||
let endpoint: string | null = request.body.endpoint as string;
|
let endpoint: string | null = request.body.endpoint as string;
|
||||||
let gymId: string | null = request.body.gymId as string;
|
let gymId: string | null = request.body.gymId as string;
|
||||||
|
|
||||||
const updateEmployeeExRequest = request.body.params as UpdateEmployeeExRequest;
|
const updateEmployeeExRequest = request.body.params as UpdateEmployeeExRequest;
|
||||||
|
|
||||||
if (!username) {
|
if (!username) {
|
||||||
throw new Error('Missing username or password');
|
throw new Error('Missing username');
|
||||||
}
|
}
|
||||||
username = username.trim();
|
username = username.trim();
|
||||||
|
|
||||||
if (!password) {
|
if (!password) {
|
||||||
if (!gymId) {
|
if (!gymId) {
|
||||||
throw new Error('Missing password or 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) {
|
if (!endpoint) {
|
||||||
throw new Error('Missing endpoint');
|
throw new Error('Missing endpoint');
|
||||||
}
|
}
|
||||||
@ -426,112 +436,125 @@ export const esslDeleteUser = onRequest({
|
|||||||
region: '#{SERVICES_RGN}#'
|
region: '#{SERVICES_RGN}#'
|
||||||
}, async (request: Request, response: Response) => {
|
}, async (request: Request, response: Response) => {
|
||||||
return corsHandler(request, response, async () => {
|
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 {
|
try {
|
||||||
new URL(endpoint);
|
let username: string | null = request.body.username as string;
|
||||||
} catch (_) {
|
let password: string | null = request.body.password as string;
|
||||||
throw new Error('Endpoint is not a valid URI or URL');
|
let endpoint: string | null = request.body.endpoint as string;
|
||||||
}
|
let gymId: string | null = request.body.gymId as string;
|
||||||
if (!endpoint.endsWith('/webservice.asmx')) {
|
const getEmployeeDetailsRequest = request.body.params as EmployeeCodeRequest;
|
||||||
if (endpoint.endsWith('/')) {
|
|
||||||
endpoint = endpoint.substring(0, endpoint.length - 1);
|
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({
|
export const esslGetEmployeePunchLogs = onRequest({
|
||||||
region: '#{SERVICES_RGN}#'
|
region: '#{SERVICES_RGN}#'
|
||||||
}, async (request: Request, response: Response) => {
|
}, async (request: Request, response: Response) => {
|
||||||
return corsHandler(request, response, async () => {
|
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 {
|
try {
|
||||||
new URL(endpoint);
|
let username: string | null = request.body.username as string;
|
||||||
} catch (_) {
|
let password: string | null = request.body.password as string;
|
||||||
throw new Error('Endpoint is not a valid URI or URL');
|
let endpoint: string | null = request.body.endpoint as string;
|
||||||
}
|
let gymId: string | null = request.body.gymId as string;
|
||||||
if (!endpoint.endsWith('/webservice.asmx')) {
|
const pushLogRequst = request.body.params as PushLogRequest;
|
||||||
if (endpoint.endsWith('/')) {
|
|
||||||
endpoint = endpoint.substring(0, endpoint.length - 1);
|
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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
51
functions/src/shared/decrypt.ts
Normal file
51
functions/src/shared/decrypt.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import * as crypto from 'crypto';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
export class RSADecryption {
|
||||||
|
private static privateKeyObject: crypto.KeyObject | null = null;
|
||||||
|
|
||||||
|
private static getPrivateKeyObject(): crypto.KeyObject {
|
||||||
|
if (!this.privateKeyObject) {
|
||||||
|
const keyPath = path.join(__dirname, '../../assets/keys/fitLien_private.pem');
|
||||||
|
const keyContent = fs.readFileSync(keyPath, 'utf8');
|
||||||
|
this.privateKeyObject = crypto.createPrivateKey({
|
||||||
|
key: keyContent,
|
||||||
|
format: 'pem'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.privateKeyObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decryptPassword(encryptedPassword: string): string {
|
||||||
|
try {
|
||||||
|
if (!encryptedPassword || encryptedPassword.trim() === '') {
|
||||||
|
throw new Error('Encrypted password cannot be empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
const privateKeyObject = this.getPrivateKeyObject();
|
||||||
|
const encryptedBuffer = Buffer.from(encryptedPassword, 'base64');
|
||||||
|
|
||||||
|
if (encryptedBuffer.length === 0) {
|
||||||
|
throw new Error('Encrypted password buffer is empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptedBuffer = crypto.privateDecrypt(
|
||||||
|
{
|
||||||
|
key: privateKeyObject,
|
||||||
|
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
|
||||||
|
oaepHash: 'sha1'
|
||||||
|
},
|
||||||
|
encryptedBuffer
|
||||||
|
);
|
||||||
|
|
||||||
|
return decryptedBuffer.toString('utf8');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Decryption error details:', {
|
||||||
|
message: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
encryptedPasswordLength: encryptedPassword?.length || 0
|
||||||
|
});
|
||||||
|
throw new Error(`Failed to decrypt password: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user