diff --git a/src/controllers/signController.ts b/src/controllers/signController.ts index c304f5c..9843e36 100644 --- a/src/controllers/signController.ts +++ b/src/controllers/signController.ts @@ -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"); @@ -309,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" }); + } +} diff --git a/src/controllers/tokenController.ts b/src/controllers/tokenController.ts index 6606cbb..d40fc13 100644 --- a/src/controllers/tokenController.ts +++ b/src/controllers/tokenController.ts @@ -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" }); + } +} diff --git a/src/controllers/transferController.ts b/src/controllers/transferController.ts index 49b0152..34dfa77 100644 --- a/src/controllers/transferController.ts +++ b/src/controllers/transferController.ts @@ -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") }); } } diff --git a/src/lib/bank/mandiri/VirtualAccountPayment.ts b/src/lib/bank/mandiri/VirtualAccountPayment.ts new file mode 100644 index 0000000..ade7e5b --- /dev/null +++ b/src/lib/bank/mandiri/VirtualAccountPayment.ts @@ -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 = { + "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, + }; +} diff --git a/src/lib/bank/mandiri/VirtualAccountStatus.ts b/src/lib/bank/mandiri/VirtualAccountStatus.ts new file mode 100644 index 0000000..8afeb61 --- /dev/null +++ b/src/lib/bank/mandiri/VirtualAccountStatus.ts @@ -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 = { + "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, + }; +} diff --git a/src/lib/bank/mandiri/generateSignatureAuth.ts b/src/lib/bank/mandiri/generateSignatureAuth.ts index 0636a3e..14c063e 100644 --- a/src/lib/bank/mandiri/generateSignatureAuth.ts +++ b/src/lib/bank/mandiri/generateSignatureAuth.ts @@ -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 = { "Content-Type": "application/json", Accept: "application/json", diff --git a/src/lib/bank/mandiri/generateSignatureService.ts b/src/lib/bank/mandiri/generateSignatureService.ts index bb21754..cfb42ed 100644 --- a/src/lib/bank/mandiri/generateSignatureService.ts +++ b/src/lib/bank/mandiri/generateSignatureService.ts @@ -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; @@ -48,5 +51,5 @@ export default async function ({ timeout: 15000, } ); - return { ...resp.data, timestamp, accessToken }; + return { ...resp.data, timestamp, accessToken, headers }; } diff --git a/src/lib/bank/mandiri/getInvoiceVirtualAccount.ts b/src/lib/bank/mandiri/getInvoiceVirtualAccount.ts index 5f3bbaf..8cbf404 100644 --- a/src/lib/bank/mandiri/getInvoiceVirtualAccount.ts +++ b/src/lib/bank/mandiri/getInvoiceVirtualAccount.ts @@ -29,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"); } @@ -42,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" diff --git a/src/middleware/vaAuth.ts b/src/middleware/vaAuth.ts index 1268209..7e63b53 100644 --- a/src/middleware/vaAuth.ts +++ b/src/middleware/vaAuth.ts @@ -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,84 +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`, }); - 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") }); } } diff --git a/src/routes/index.ts b/src/routes/index.ts index faaea84..8b1cc4f 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -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