Compare commits

...

3 Commits

11 changed files with 288 additions and 99 deletions

View File

@ -5,6 +5,7 @@ import crypto from "crypto";
import db from "../db";
import { get, set } from "lodash";
import { hashToken } from "../middleware/auth";
import generateSignatureService from "../lib/bank/mandiri/generateSignatureService";
const KEY_PATH =
process.env.PRIVATE_KEY_PATH || path.join(__dirname, "../../secrets/key.pem");
@ -112,7 +113,6 @@ export async function createSignature({
pattern: string;
passphrase?: string;
}) {
console.log(data);
if (data?.accesstoken) {
const session = await db.sessions.findFirst({
where: { token_hash: hashToken(data.accesstoken) },
@ -310,3 +310,32 @@ export async function verifySignature(req: Request, res: Response) {
return res.status(500).json({ error: err.message || "internal error" });
}
}
export async function generateSignatureBank(req: Request, res: Response) {
try {
const data = req.body || {};
const clientId = get(data, "client_id") || get(data, "clientId");
const user = await db.users.findFirst({
where: { clientbank_id: clientId },
include: { database: true, parameters: true, tenants: true, banks: true },
});
const signatureService = await generateSignatureService({
data: {
isGenerateNewToken: true,
client_secret: get(data, "client_secret") || "",
endpointurl: get(data, "endpointurl") || "",
body: get(data, "requestBody", {}),
},
user: user as any,
});
return res.json({
...signatureService,
algorithm: "RSA-SHA256",
client_secret: get(data, "client_secret") || "",
endpointurl: get(data, "endpointurl") || "",
body: get(data, "requestBody", {}),
});
} catch (err: any) {
console.error("generateSignature error", err);
return res.status(500).json({ error: err.message || "internal error" });
}
}

View File

@ -5,6 +5,9 @@ import path from "path";
import { hashToken } from "../middleware/auth";
import db from "../db";
import { formatTimestamp } from "../lib/formatTimestamp";
import callGate from "../lib/gate";
import { getParameter } from "../lib/getParameter";
import { get } from "lodash";
function loadCertForUser(user: any): string | null {
// try deriving .cer from private_key_file
@ -179,3 +182,56 @@ export async function b2bAccessToken(req: Request, res: Response) {
.json({ responseCode: "5000000", responseMessage: "internal error" });
}
}
export async function signatureBank(req: Request, res: Response) {
try {
const clientKey = (req.headers["x-token-key"] ||
req.headers["X-TOKEN-KEY"] ||
req.headers["x-token-key" as any]) as string;
const data = req.body || {};
const requiredHeader = ["X-TOKEN-KEY"];
const requiredBody = ["endpointurl", "requestBody"];
for (const h of requiredHeader) {
if (!req.headers[h.toLowerCase()]) {
return res.status(400).json({ error: `missing header ${h}` });
}
}
for (const b of requiredBody) {
if (!data[b]) {
return res.status(400).json({ error: `missing body ${b}` });
}
}
const user = await db.users.findFirst({
where: { token_access: clientKey },
include: {
parameters: true,
},
});
if (!user) {
return res.status(401).json({
responseCode: "4017300",
responseMessage: "Unauthorized. Client ID not found",
});
}
const signature = await callGate({
client: "mandiri",
action: "generateSignatureService",
data: {
isGenerateNewToken: true,
client_secret: getParameter(user, "client_secret") || "",
endpointurl: get(data, "endpointurl", ""),
body: get(data, "requestBody", {}),
},
user,
});
return res.json({
...signature,
});
} catch (err: any) {
console.error("b2bAccessToken error", err);
return res
.status(500)
.json({ responseCode: "5000000", responseMessage: "internal error" });
}
}

View File

@ -1,5 +1,4 @@
import { Request, Response } from "express";
import callGateFromReq from "../lib/gateClient";
import getTokenAuth from "../lib/getTokenAuth";
import { getUserFromToken } from "../middleware/auth";
import { dbQueryClient } from "../lib/dbQueryClient";
@ -29,11 +28,26 @@ export async function inquiry(req: Request, res: Response) {
return res.status(400).json({ error: `missing body field ${f}` });
}
}
const result = await callGateFromReq(req, {
const parameters = await db.parameters.findFirst({
where: {
param_key: "partnerServiceId",
param_value: get(body, "partnerServiceId"),
},
});
const user = await db.users.findFirst({
where: { user_id: parameters?.user_id },
include: {
database: true,
banks: true,
parameters: true,
tenants: true,
},
});
const result = await callGate({
client: "mandiri",
action: "getInvoiceVirtualAccount",
data: { ...body },
user: user,
});
return res.status(200).json({
@ -46,7 +60,9 @@ export async function inquiry(req: Request, res: Response) {
}
} catch (err) {
console.error("transfer inquiry error:", err);
return res.status(500).json({ error: "internal_error" });
return res
.status(500)
.json({ error: "internal_error", message: get(err, "message") });
}
}

View File

@ -0,0 +1,56 @@
import { get } from "lodash";
import { db } from "../../../db";
import axios from "axios";
import { formatTimestamp } from "../../formatTimestamp";
import generateSignatureService from "./generateSignatureService";
import { getParameter } from "../../getParameter";
export default async function ({ data }: { data: any }) {
const endpointurl = "/api/v1.0/transfer-va/payment";
const user = await db.users.findFirst({
where: {
clientbank_id: data.clientbank_id,
},
include: { banks: true, database: true, parameters: true },
});
if (!user) throw new Error("User not found");
const timestamp = formatTimestamp();
const signatureService = await generateSignatureService({
data: {
accessToken: data.accessToken,
timestamp,
isGenerateNewToken: true,
client_secret: getParameter(user, "client_secret") || "",
endpointurl,
body: get(data, "requestBody", {}),
},
user,
});
data.accessToken = signatureService.accessToken;
data.timestamp = signatureService.timestamp;
data.signature = signatureService.signature;
const clientId = data.clientbank_id;
const headers: Record<string, string> = {
"Content-Type": "application/json",
Accept: "application/json",
"X-PARTNER-ID": clientId,
"X-TIMESTAMP": data.timestamp,
"X-SIGNATURE": data.signature,
Authorization: `Bearer ${data.accessToken}`,
"X-EXTERNAL-ID": data.external_id,
"CHANNEL-ID": data.channel_id,
};
const mandiriUrl = String(get(user, "endpoint") || process.env.PAYMENT_URL);
const resp = await axios.post(
mandiriUrl + endpointurl,
{ ...get(data, "requestBody", {}) },
{
headers,
timeout: 15000,
}
);
return {
...resp.data,
timestamp: data.timestamp,
};
}

View File

@ -0,0 +1,56 @@
import { get } from "lodash";
import { db } from "../../../db";
import axios from "axios";
import { formatTimestamp } from "../../formatTimestamp";
import generateSignatureService from "./generateSignatureService";
import { getParameter } from "../../getParameter";
export default async function ({ data }: { data: any }) {
const endpointurl = "/api/v1.0/transfer-va/status";
const user = await db.users.findFirst({
where: {
clientbank_id: data.clientbank_id,
},
include: { banks: true, database: true, parameters: true },
});
if (!user) throw new Error("User not found");
const timestamp = formatTimestamp();
const signatureService = await generateSignatureService({
data: {
accessToken: data.accessToken,
timestamp,
isGenerateNewToken: true,
client_secret: getParameter(user, "client_secret") || "",
endpointurl,
body: get(data, "requestBody", {}),
},
user,
});
data.accessToken = signatureService.accessToken;
data.timestamp = signatureService.timestamp;
data.signature = signatureService.signature;
const clientId = data.clientbank_id;
const headers: Record<string, string> = {
"Content-Type": "application/json",
Accept: "application/json",
"X-PARTNER-ID": clientId,
"X-TIMESTAMP": data.timestamp,
"X-SIGNATURE": data.signature,
Authorization: `Bearer ${data.accessToken}`,
"X-EXTERNAL-ID": data.external_id,
"CHANNEL-ID": data.channel_id,
};
const mandiriUrl = String(get(user, "endpoint") || process.env.PAYMENT_URL);
const resp = await axios.post(
mandiriUrl + endpointurl,
{ ...get(data, "requestBody", {}) },
{
headers,
timeout: 15000,
}
);
return {
...resp.data,
timestamp: data.timestamp,
};
}

View File

@ -12,7 +12,9 @@ export default async function ({ data }: { data: any }) {
});
if (!user) throw new Error("User not found");
const clientId = data.clientbank_id;
const timestamp = formatTimestamp();
const timestamp = get(data, "isNewTimestamp", true)
? formatTimestamp()
: get(data, "timestamp", formatTimestamp());
const headers: Record<string, any> = {
"Content-Type": "application/json",
Accept: "application/json",
@ -21,10 +23,7 @@ export default async function ({ data }: { data: any }) {
Private_Key: getParameter(user, "private_key") || "",
};
const mandiriUrl = String(get(user, "endpoint") || process.env.PAYMENT_URL);
console.log({
headers,
url: `${mandiriUrl}/api/v1.0/utilities/signature-auth`,
});
const resp = await axios.post(
`${mandiriUrl}/api/v1.0/utilities/signature-auth`,
{},

View File

@ -13,10 +13,13 @@ export default async function ({
const isGenerateNewToken = get(data, "isGenerateNewToken", false);
let timestamp = data.timestamp;
let accessToken = data.accessToken;
let isNewTimestamp = get(data, "isNewTimestamp", true);
if (isGenerateNewToken) {
const signatureAuth = await generateSignatureAuth({
data: {
clientbank_id: user.clientbank_id,
timestamp: timestamp,
isNewTimestamp,
},
});
timestamp = signatureAuth.timestamp;
@ -40,11 +43,6 @@ export default async function ({
AccessToken: accessToken,
};
const mandiriUrl = String(get(user, "endpoint") || process.env.PAYMENT_URL);
console.log({
url: `${mandiriUrl}/api/v1.0/utilities/signature-service`,
headers,
body: { ...get(data, "body", {}) },
});
const resp = await axios.post(
`${mandiriUrl}/api/v1.0/utilities/signature-service`,
{ ...get(data, "body", {}) },
@ -53,5 +51,5 @@ export default async function ({
timeout: 15000,
}
);
return { ...resp.data, timestamp, accessToken };
return { ...resp.data, timestamp, accessToken, headers };
}

View File

@ -21,7 +21,6 @@ export default async function ({
session: any;
data: any;
}) {
console.log(user);
// sample implementation — adapt to real bank payload and API calls
const { invoiceId } = data || {};
if (!user.database) throw new Error("User database information is missing");
@ -30,6 +29,7 @@ export default async function ({
virtualAccountNo: invoiceId,
dbName: get(user, "database.name", ""),
});
if (!c_bpartner) {
throw new Error("C_BPartner_ID not found for the given Virtual Account No");
}
@ -43,6 +43,16 @@ export default async function ({
is_pay: "N",
},
});
// console.log({
// _sum: {
// grandtotal: true,
// },
// where: {
// c_bpartner_id: c_bpartner.c_bpartner_id,
// db_id: user.database.db_id,
// is_pay: "N",
// },
// });
if (lo.get(grandtotal, "_sum.grandtotal") === null) {
throw new Error(
"No outstanding amount found for the given Virtual Account No"

View File

@ -45,9 +45,7 @@ export default async function ({
.replace("{accesstoken}", get(data, "accesstoken"))
.replace("{requestbody}", hashBody)
.replace("{timestamp}", get(data, "timestamp"));
console.log(stringToSign);
const pathPublicKey = user.public_key_file;
console.log(user);
if (!pathPublicKey) {
throw new Error("No public key file associated with the user");
}
@ -63,11 +61,7 @@ export default async function ({
throw new Error("Public key file not found");
}
const cerPem = readCertAsPEM(fullPath);
console.log({
stringToVerify: stringToSign,
signatureBase64: get(data, "signature"),
certPem: cerPem,
});
const result = verifyRsaSignature({
stringToVerify: stringToSign,
signatureBase64: get(data, "signature"),

View File

@ -1,8 +1,8 @@
import { Request, Response, NextFunction } from "express";
import { authMiddleware, hashToken } from "./auth";
import { db } from "../db";
import callGateFromReq from "../lib/gateClient";
import { get } from "lodash";
import generateSignatureService from "../lib/bank/mandiri/generateSignatureService";
import { getParameter } from "../lib/getParameter";
export async function vaMiddleware(
req: Request,
@ -20,85 +20,53 @@ export async function vaMiddleware(
if (!token) {
return res.status(401).json({ error: "missing token" });
}
const session = await db.sessions.findFirst({
where: { token_hash: hashToken(token), is_revoked: false },
include: { users: true },
const data = req.body || {};
if (!get(data, "partnerServiceId")) {
return res.status(400).json({
responseCode: "2002400",
responseMessage: `Invalid Mandatory Field partnerServiceId`,
});
}
const parameters = await db.parameters.findFirst({
where: {
param_key: "partnerServiceId",
param_value: get(data, "partnerServiceId"),
},
});
if (!session) {
return res.status(401).json({ error: "invalid token" });
}
if (!timestamp || !signature) {
return res.status(400).json({ error: "missing required headers" });
}
try {
const result = await callGateFromReq(req, {
client: "mandiri",
action: "verifySignature",
data: {
timestamp,
signature,
httpmethod: req.method === "POST" ? "POST" : "GET",
endpointurl: req.originalUrl,
accesstoken: token,
requestbody: req.body,
typeVerify: "transaction",
},
if (!parameters) {
return res.status(404).json({
responseCode: "4042412",
responseMessage: `Invalid partnerServiceId`,
});
console.log({ result });
if (!result) {
return res.status(401).json({
responseCode: "4012400",
responseMessage: `Unauthorized`,
});
}
} catch (err) {
const message: any = get(err, "message", "internal_error");
if (
message === "Invalid access token" ||
message === "Access token expired"
) {
return res.status(401).json({
responseCode: "4012400",
responseMessage: `Unauthorized ${message}`,
});
} else {
console.error("vaMiddleware error", err);
return res.status(500).json({
responseCode: "5002401",
responseMessage: `Internal Server Error ${message}`,
});
}
}
// Prefer RSA verification if the user record contains a public cert
const user = session.users;
// some projects store certs in different user columns; access via any to avoid TS errors
const uAny = user as any;
const certPem = (uAny &&
(uAny.public_cert ||
uAny.cert_pem ||
uAny.certificate ||
uAny.publicCert)) as string | undefined;
const clientId = (uAny &&
(uAny.client_id ||
uAny.clientbank_id ||
uAny.clientKey ||
uAny.clientkey)) as string | undefined;
let ok = false;
if (authHeader) {
const token = authHeader.slice("Bearer ".length).trim();
const tokenHash = hashToken(token);
const session = await db.sessions.findFirst({
where: { token_hash: tokenHash, is_revoked: false },
const users = await db.users.findFirst({
where: { user_id: parameters.user_id },
include: { database: true, banks: true, parameters: true, tenants: true },
});
const signatureService = await generateSignatureService({
data: {
accessToken: token,
timestamp: timestamp,
isNewTimestamp: false,
isGenerateNewToken: false,
client_secret: getParameter(users, "client_secret") || "",
endpointurl: req.originalUrl,
body: data,
},
user: users as any,
});
const signatureServiceResult = signatureService.signature;
if (signatureServiceResult !== signature) {
return res.status(401).json({
responseCode: "4012400",
responseMessage: `Unauthorized. Invalid signature`,
});
// If we have an auth header, we're good
return authMiddleware(req as any, res, next as any);
} else {
return res.status(401).json({ error: "missing token" });
}
next();
} catch (err) {
console.error("vaMiddleware error", err);
return res.status(500).json({ error: "internal_error" });
return res
.status(500)
.json({ error: "internal_error", message: get(err, "message") });
}
}

View File

@ -78,6 +78,10 @@ export const setRoutes = () => {
router.post("/generate-signature", (req, res) =>
signController.generateSignature(req, res)
);
// Signature generation for testing (uses PRIVATE_KEY_PATH file)
router.post("/generate-signature-bank", (req, res) =>
signController.generateSignature(req, res)
);
// Verify signature using public cert (default: public/mandiri/midsuit.cer)
router.post("/verify-signature", (req, res) =>
signController.verifySignature(req, res)
@ -110,6 +114,9 @@ export const setRoutes = () => {
versionRouter.post("/access-token/b2b", (req, res) =>
tokenController.b2bAccessToken(req, res)
);
versionRouter.post("/utilities/signature-service", (req, res) =>
tokenController.signatureBank(req, res)
);
router.use("/:version", versionRouter);
// Sync endpoints group