diff --git a/.gitignore b/.gitignore index 28d7714..713d500 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ node_modules/ -.vscode/ -.env/ +.env diff --git a/src/app.js b/src/app.js index 3c86b46..e666308 100644 --- a/src/app.js +++ b/src/app.js @@ -1,6 +1,6 @@ const express = require('express'); const cors = require('cors'); -const { logger } = require('./util/logger'); +const { logger, requestLogger } = require('./util/logger'); const routes = require('./routes'); const app = express(); @@ -9,6 +9,26 @@ app.use(cors()); app.use(express.json()); app.use(express.urlencoded({ extended: true })); +app.use((req, res, next) => { + const start = Date.now(); + + res.on('finish', () => { + requestLogger.info( + { + ip: req.ip || req.connection.remoteAddress, + method: req.method, + url: req.originalUrl, + headers: req.headers, + body: req.body, + status: res.statusCode, + responseTime: `${Date.now() - start}ms`, + }, + 'Incoming request' + ); + }); + next(); +}); + app.use('/api', routes); app.get('/health', (_, res) => res.send('server is healthy')); app.use((err, _req, res, _next) => { @@ -17,3 +37,4 @@ app.use((err, _req, res, _next) => { }); module.exports = app; + diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index 9a90add..94e23f8 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -14,7 +14,8 @@ async function login(req, res) { const currentTime = new Date().toISOString(); try { const user = await authService.validateUser(customerNo, password); - if (!user) return res.status(401).json({ error: 'invalid credentials' }); + if (!user || !password) + return res.status(401).json({ error: 'invalid credentials' }); const token = generateToken(user.customer_no, '1d'); const FirstTimeLogin = await authService.CheckFirstTimeLogin(customerNo); await db.query('UPDATE users SET last_login = $1 WHERE customer_no = $2', [ @@ -34,7 +35,6 @@ async function fetchUserDetails(req, res) { const user = await authService.findUserByCustomerNo(customerNo); if (!user) return res.status(404).json({ message: 'USER_NOT_FOUND' }); return res.json(user); - } catch (err) { logger.error(err, 'error occured while fetching user details'); res.status(500).json({ error: 'something went wrong' }); @@ -102,4 +102,11 @@ async function setTransactionPassword(req, res) { } } -module.exports = { login, tpin, setTpin, setLoginPassword, setTransactionPassword,fetchUserDetails }; +module.exports = { + login, + tpin, + setTpin, + setLoginPassword, + setTransactionPassword, + fetchUserDetails, +}; diff --git a/src/controllers/beneficiary.controller.js b/src/controllers/beneficiary.controller.js index ccac3bf..b59d2db 100644 --- a/src/controllers/beneficiary.controller.js +++ b/src/controllers/beneficiary.controller.js @@ -95,6 +95,24 @@ async function getBeneficiary(req, res) { } } +async function deleteBeneficiary(req, res) { + const { beneficiaryAccountNo } = req.params; + try { + await beneficiaryService.deleteBeneficiary(req.user, beneficiaryAccountNo); + res.status(204).send(); + } catch (error) { + if (error.message === 'ACCOUNT_NOT_FOUND') { + logger.warn( + `beneficiary ${beneficiaryAccountNo} does not exist for the customer ${req.user}` + ); + return res.status(400).json({ error: 'INVALID_BENEFICIARY_ACCOUNT_NO' }); + } else { + logger.error(error, 'error deleting beneficiary'); + return res.status(500).json({ error: 'INTERNAL_SERVER_ERROR' }); + } + } +} + async function getIfscDetails(req, res) { const { ifscCode } = req.query; if (!ifscCode) { @@ -127,4 +145,5 @@ module.exports = { addBeneficiary, getIfscDetails, getBeneficiary, + deleteBeneficiary, }; diff --git a/src/controllers/neft.controller.js b/src/controllers/neft.controller.js index 091b7a3..8cc47dc 100644 --- a/src/controllers/neft.controller.js +++ b/src/controllers/neft.controller.js @@ -30,6 +30,8 @@ async function send( 'API call failed: ' + (error.response?.data?.message || error.message) ); } + + } module.exports = { send }; diff --git a/src/controllers/otp.controller.js b/src/controllers/otp.controller.js new file mode 100644 index 0000000..b3037b5 --- /dev/null +++ b/src/controllers/otp.controller.js @@ -0,0 +1,98 @@ + +const { setJson, getJson } = require('../config/redis'); +const { generateOTP } = require('../otpgenerator'); +const { logger } = require('../util/logger'); +const axios = require('axios'); +const templates = require('../util/sms_template'); + +// Send OTP +async function SendOtp(req, res) { + const { mobileNumber, type, amount, beneficiary, ifsc, acctFrom, acctTo, ref, date } = req.body; + + if (!mobileNumber || !type) { + return res.status(400).json({ error: 'Mobile number and type are required' }); + } + + try { + const otp = generateOTP(6); + let message; + + // Pick template based on type + switch (type) { + case 'IMPS': + message = templates.IMPS(otp); + break; + case 'NEFT': + message = templates.NEFT(otp, amount, beneficiary); + break; + case 'RTGS': + message = templates.RTGS(otp, amount, beneficiary); + break; + case 'BENEFICIARY_ADD': + message = templates.BENEFICIARY_ADD(otp, beneficiary, ifsc); + break; + case 'BENEFICIARY_SUCCESS': + message = templates.BENEFICIARY_SUCCESS(beneficiary); + break; + case 'NOTIFICATION': + message = templates.NOTIFICATION(acctFrom, acctTo, amount, ref, date); + break; + case 'FORGOT_PASSWORD': + message = templates.FORGOT_PASSWORD(otp); + break; + default: + return res.status(400).json({ error: 'Invalid OTP type' }); + } + + // Call SMS API + const response = await axios.post('http://localhost:9999/api/SendtoMessage', { + mobileNumber, + stMessage: message, + }); + + if (response.data) { + // Save OTP only if it's OTP based (skip notifications without OTP) + if (message.includes('OTP')) { + await setJson(`otp:${mobileNumber}`, otp, 300); + } + + logger.info(`Sent OTP [${otp}] for type [${type}] to ${mobileNumber}`); + } + + return res.status(200).json({ message: 'Message sent successfully' }); + + } catch (err) { + logger.error(err, 'Error sending OTP'); + return res.status(500).json({ error: 'Internal server error' }); + } +} + + +// Verify OTP +async function VerifyOtp(req, res) { + const { mobileNumber } = req.query; + const {otp} =req.body + + if (!mobileNumber || !otp) { + return res.status(400).json({ error: 'Phone number and OTP are required' }); + } + + try { + const storedOtp = await getJson(`otp:${mobileNumber}`); + + if (!storedOtp) { + return res.status(400).json({ error: 'OTP expired or not found' }); + } + + if (parseInt(otp, 10) !== parseInt(storedOtp, 10)) { + return res.status(400).json({ error: 'Invalid OTP' }); + } + + return res.status(200).json({ message: 'OTP verified successfully' }); + } catch (err) { + logger.error(err, 'Error verifying OTP'); + return res.status(500).json({ error: 'Internal server error' }); + } +} + +module.exports = { SendOtp, VerifyOtp }; diff --git a/src/otpgenerator.js b/src/otpgenerator.js new file mode 100644 index 0000000..b66a46b --- /dev/null +++ b/src/otpgenerator.js @@ -0,0 +1,12 @@ + +function generateOTP(length) { + const digits = '0123456789'; + let otp = ''; + otp += digits[Math.floor(Math.random() * 9) + 1]; // first digit cannot be zero + for (let i = 1; i < length; i++) { + otp += digits[Math.floor(Math.random() * digits.length)]; + } + return otp; +} + +module.exports = { generateOTP }; diff --git a/src/routes/beneficiary.route.js b/src/routes/beneficiary.route.js index d1483d8..47e7eba 100644 --- a/src/routes/beneficiary.route.js +++ b/src/routes/beneficiary.route.js @@ -9,5 +9,9 @@ router.get('/validate/outside-bank', beneficiaryController.validateOutsideBank); router.get('/ifsc-details', beneficiaryController.getIfscDetails); router.get('/', beneficiaryController.getBeneficiary); router.post('/', newBeneficiaryValidator, beneficiaryController.addBeneficiary); +router.delete( + '/:beneficiaryAccountNo', + beneficiaryController.deleteBeneficiary +); module.exports = router; diff --git a/src/routes/index.js b/src/routes/index.js index 429e61d..a779232 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -10,6 +10,7 @@ const neftRoute = require('./neft.route'); const rtgsRoute = require('./rtgs.route'); const impsRoute = require('./imps.route'); const { npciResponse } = require('../controllers/npci.controller'); +const otp = require('./otp.route'); const router = express.Router(); @@ -23,6 +24,7 @@ router.use('/payment/rtgs', authenticate, rtgsRoute); router.use('/payment/imps', authenticate, impsRoute); router.use('/beneficiary', authenticate, beneficiaryRoute); router.use('/npci/beneficiary-response', npciResponse); +router.use('/otp', otp); module.exports = router; diff --git a/src/routes/otp.route.js b/src/routes/otp.route.js new file mode 100644 index 0000000..fdf2a7d --- /dev/null +++ b/src/routes/otp.route.js @@ -0,0 +1,13 @@ + +const express = require('express'); +const otpController = require('../controllers/otp.controller'); + +const router = express.Router(); + +// Send OTP (POST request with body) +router.post('/send', otpController.SendOtp); + +// Verify OTP (GET request with query params ?mobileNumber=xxx&otp=123456) +router.post('/verify', otpController.VerifyOtp); + +module.exports = router; diff --git a/src/services/beneficiary.service.js b/src/services/beneficiary.service.js index 136f3d1..1eeb7d1 100644 --- a/src/services/beneficiary.service.js +++ b/src/services/beneficiary.service.js @@ -44,6 +44,16 @@ async function getSingleBeneficiary(customerNo, accountNo) { return result.rows[0]; } +async function deleteBeneficiary(customerNo, beneficiaryAccountNo) { + const queryStr = + 'DELETE FROM beneficiaries WHERE customer_no = $1 AND account_no = $2'; + const result = await db.query(queryStr, [customerNo, beneficiaryAccountNo]); + if (result.rowCount == 0) { + throw new Error('ACCOUNT_NOT_FOUND'); + } + return; +} + async function getAllBeneficiaries(customerNo) { const queryStr = 'SELECT b.account_no, b.name, b.account_type, b.ifsc_code, i.bank_name, i.branch_name FROM beneficiaries b JOIN ifsc_details i ON b.ifsc_code = i.ifsc_code WHERE customer_no = $1'; @@ -66,4 +76,5 @@ module.exports = { validateOutsideBank, getAllBeneficiaries, getSingleBeneficiary, + deleteBeneficiary, }; diff --git a/src/util/logger.js b/src/util/logger.js index eb033b0..18c891f 100644 --- a/src/util/logger.js +++ b/src/util/logger.js @@ -1,6 +1,19 @@ const pino = require('pino'); +const fs = require('fs'); +const path = require('path'); + const isDev = process.env.NODE_ENV !== 'production'; +const logDir = path.join(__dirname, '../..', 'logs'); +if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir); +} + +const requestLoggerStream = pino.destination({ + dest: path.join(logDir, 'requests.log'), + sync: false, +}); + const logger = pino({ transport: isDev ? { @@ -15,8 +28,12 @@ const logger = pino({ level: isDev ? 'debug' : 'info', }); -const requestLogger = (req, _res, next) => { - logger.info(`${req.method} ${req.url}`); - next(); -}; +const requestLogger = pino( + { + level: 'info', + base: null, + }, + requestLoggerStream +); + module.exports = { logger, requestLogger }; diff --git a/src/util/sms_template.js b/src/util/sms_template.js new file mode 100644 index 0000000..9afb6e3 --- /dev/null +++ b/src/util/sms_template.js @@ -0,0 +1,23 @@ +const templates = { + IMPS: (otp) => `Dear Customer, Please complete the fund transfer with OTP ${otp} -KCCB`, + + NEFT: (otp, amount, beneficiary) => + `Dear Customer, Please complete the NEFT of Rs.${amount} to ${beneficiary} with OTP:${otp} -KCCB`, + + RTGS: (otp, amount, beneficiary) => + `Dear Customer, Please complete the RTGS of Rs.${amount} to ${beneficiary} with OTP:${otp} -KCCB`, + + BENEFICIARY_ADD: (otp, beneficiary, ifsc) => + `Dear Customer, You have added beneficiary ${beneficiary} ${ifsc} for NEFT/RTGS. Please endorse the beneficiary with OTP ${otp} -KCCB`, + + BENEFICIARY_SUCCESS: (beneficiary) => + `Dear Customer, Your Beneficiary: ${beneficiary} for Net Banking is added successfully -KCCB`, + + NOTIFICATION: (acctFrom, acctTo, amount, ref, date) => + `Your A/c ${acctFrom} is debited for Rs. ${amount} to the credit of A/c ${acctTo} thru Net Banking - ref: ${ref} - ${date} - Kangra Central Co-Operative Bank -KCCB`, + + FORGOT_PASSWORD: (otp) => + `Dear Customer, Forgot Password OTP is ${otp} -KCCB`, +}; + +module.exports = templates; \ No newline at end of file