diff --git a/openapi.yml b/openapi.yml new file mode 100644 index 0000000..18dd702 --- /dev/null +++ b/openapi.yml @@ -0,0 +1,35 @@ +openapi: 3.0.3 +info: + title: kmobile + description: backend for mobile banking application + version: 1.0.0 + servers: + - url: http://api.example.com + paths: + /auth/login: + post: + summary: Login a user + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + username: + type: string + password: + type: string + required: [username, password] + responses: + '200': + description: successful login + content: + application/json: + schema: + type: object + properties: + token: + type: string + '401': + description: invalid credentials diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index 1dfb88a..33ff3cd 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -1,4 +1,4 @@ -const { validateUser } = require('../services/auth.service'); +const authService = require('../services/auth.service'); const { generateToken } = require('../util/jwt'); const { logger } = require('../util/logger'); @@ -12,9 +12,9 @@ async function login(req, res) { } try { - const user = await validateUser(customerNo, password); + const user = await authService.validateUser(customerNo, password); if (!user) return res.status(401).json({ error: 'invalid credentials' }); - const token = generateToken(user.customer_no); + const token = generateToken(user.customer_no, '1d'); res.json({ token }); } catch (err) { logger.error(err, 'login failed'); @@ -22,4 +22,38 @@ async function login(req, res) { } } -module.exports = { login }; +async function tpin(req, res) { + const customerNo = req.user; + try { + const user = await authService.findUserByCustomerNo(customerNo); + if (!user) return res.status(404).json({ message: 'USER_NOT_FOUND' }); + if (!user.tpin) { + return res.json({ tpinSet: false }); + } else { + return res.json({ tpinSet: true }); + } + } catch (err) { + logger.error(err, 'error occured while checking tpin'); + res.status(500).json({ error: 'something went wrong' }); + } +} + +async function setTpin(req, res) { + const customerNo = req.user; + try { + const user = await authService.findUserByCustomerNo(customerNo); + if (!user) return res.status(404).json({ error: 'USER_NOT_FOUND' }); + if (user.tpin) + return res.status(400).json({ error: 'USER_ALREADY_HAS_A_TPIN' }); + const { tpin } = req.body; + if (!/^\d{6}$/.test(tpin)) + return res.status(400).json({ error: 'INVALID_TPIN_FORMAT' }); + authService.setTpin(customerNo, tpin); + return res.json({ message: 'TPIN_SET' }); + } catch (error) { + logger.error(error); + return res.status(500).json({ error: 'SOMETHING_WENT_WRONG' }); + } +} + +module.exports = { login, tpin, setTpin }; diff --git a/src/controllers/customer_details.controller.js b/src/controllers/customer_details.controller.js index 00759a3..55f9c14 100644 --- a/src/controllers/customer_details.controller.js +++ b/src/controllers/customer_details.controller.js @@ -8,7 +8,6 @@ async function getDetails(customerNo) { { params: { stcustno: customerNo } } ); const details = response.data; - logger.info(details, 'response from cbs'); const processedDetails = details.map((acc) => ({ ...acc, activeAccounts: details.length, @@ -16,6 +15,7 @@ async function getDetails(customerNo) { })); return processedDetails; } catch (error) { + logger.error('while fetching customer details', error); throw new Error( 'API call failed: ' + (error.response?.data?.message || error.message) ); diff --git a/src/controllers/transfer.controller.js b/src/controllers/transfer.controller.js new file mode 100644 index 0000000..ea02605 --- /dev/null +++ b/src/controllers/transfer.controller.js @@ -0,0 +1,29 @@ +const axios = require('axios'); + +async function transfer( + fromAccountNo, + toAccountNo, + toAccountType, + amount, + narration = 'tranfer from mobile' +) { + try { + const response = await axios.post( + 'http://localhost:8689/kccb/Interbankfundtranfer', + { + fromAccountNo, + toAccountNo, + toAccountType, + amount, + narration, + } + ); + return response.data; + } catch (error) { + throw new Error( + 'API call failed: ' + (error.response?.data?.message || error.message) + ); + } +} + +module.exports = { transfer }; diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js index 7632518..ee2b4c2 100644 --- a/src/middlewares/auth.middleware.js +++ b/src/middlewares/auth.middleware.js @@ -14,7 +14,7 @@ function auth(req, res, next) { try { const payload = verifyToken(token); - req.user = payload; + req.user = payload.customerNo; next(); } catch (err) { logger.error(err, 'error verifying token'); diff --git a/src/routes/auth.route.js b/src/routes/auth.route.js index e69de29..11981c5 100644 --- a/src/routes/auth.route.js +++ b/src/routes/auth.route.js @@ -0,0 +1,11 @@ +const authController = require('../controllers/auth.controller'); +const authenticate = require('../middlewares/auth.middleware'); +const express = require('express'); + +const router = express.Router(); + +router.post('/login', authController.login); +router.get('/tpin', authenticate, authController.tpin); +router.post('/tpin', authenticate, authController.setTpin); + +module.exports = router; diff --git a/src/routes/customer_details.route.js b/src/routes/customer_details.route.js index e69de29..7e1114d 100644 --- a/src/routes/customer_details.route.js +++ b/src/routes/customer_details.route.js @@ -0,0 +1,15 @@ +const customerController = require('../controllers/customer_details.controller'); +const { logger } = require('../util/logger'); + +const customerRoute = async (req, res) => { + const customerNo = req.user; + try { + const details = await customerController.getDetails(customerNo); + return res.json(details); + } catch (err) { + logger.error(err); + return res.status(500).json({ message: 'INTERNAL_SERVER_ERROR' }); + } +}; + +module.exports = customerRoute; diff --git a/src/routes/index.js b/src/routes/index.js index dd05447..16ac5c8 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -2,11 +2,14 @@ const express = require('express'); const authRoute = require('./auth.route'); const detailsRoute = require('./customer_details.route'); const transactionRoute = require('./transactions.route'); +const authenticate = require('../middlewares/auth.middleware'); +const transferRoute = require('./transfer.route'); const router = express.Router(); router.use('/auth', authRoute); -router.use('/customer', detailsRoute); -router.use('/transactions', transactionRoute); +router.use('/customer', authenticate, detailsRoute); +router.use('/transactions/account/:accountNo', authenticate, transactionRoute); +router.use('/payment/transfer', authenticate, transferRoute); module.exports = router; diff --git a/src/routes/transactions.route.js b/src/routes/transactions.route.js index e69de29..9e4ff8c 100644 --- a/src/routes/transactions.route.js +++ b/src/routes/transactions.route.js @@ -0,0 +1,17 @@ +const transactionsController = require('../controllers/transactions.controller'); +const { logger } = require('../util/logger'); + +const transactionsRoute = async (req, res) => { + const accountNo = req.params.accountNo; + try { + const data = await transactionsController.getLastTen(accountNo); + return res.json(data); + } catch (error) { + logger.error('error retriving last 10 txns', error); + return res + .status(500) + .json({ message: 'error occured while fetching transactions' }); + } +}; + +module.exports = transactionsRoute; diff --git a/src/routes/transfer.route.js b/src/routes/transfer.route.js new file mode 100644 index 0000000..e61b42a --- /dev/null +++ b/src/routes/transfer.route.js @@ -0,0 +1,45 @@ +const transferController = require('../controllers/transfer.controller'); +const { logger } = require('../util/logger'); +const express = require('express'); +const tpinValidator = require('../validators/tpin.validator'); +const transferValidator = require('../validators/transfer.validator'); + +const router = express.Router(); +router.use(tpinValidator, transferValidator); + +const transferRoute = async (req, res) => { + const { fromAccount, toAccount, toAccountType, amount } = req.body; + try { + const result = await transferController.transfer( + fromAccount, + toAccount, + toAccountType, + amount + ); + + if (result.status === 'O.K.') { + return res.json({ message: 'TRANSACTION_SUCCESS' }); + } else if (result.status.includes('INVALID CHECK DIGIT')) { + return res + .status(400) + .json({ error: 'INVALID_ACCOUNT_NO', status: result.status }); + } else if ( + result.status.includes('CLEARED BAL/FUNDS/DP NOT AVAILABLE.CARE') + ) { + return res + .status(400) + .json({ error: 'INSUFFICIENT_BALANCE', status: result.status }); + } else { + return res + .status(400) + .json({ error: 'PROBLEM_TRANSFERRING_FUNDS', status: result.status }); + } + } catch (error) { + logger.error(error, 'error occured while doing transfer'); + return res.status(500).json({ error: 'INTERNAL_SERVER_ERROR' }); + } +}; + +router.post('/', transferRoute); + +module.exports = router; diff --git a/src/services/auth.service.js b/src/services/auth.service.js index be03090..003bdfc 100644 --- a/src/services/auth.service.js +++ b/src/services/auth.service.js @@ -1,5 +1,5 @@ const db = require('../config/db'); -const { comparePassword } = require('../util/hash'); +const { comparePassword, hashPassword } = require('../util/hash'); async function findUserByCustomerNo(customerNo) { const result = await db.query('SELECT * FROM users WHERE customer_no = $1', [ @@ -15,4 +15,25 @@ async function validateUser(customerNo, password) { return isMatch ? user : null; } -module.exports = { validateUser }; +async function validateTpin(customerNo, tpin) { + const user = await findUserByCustomerNo(customerNo); + if (!user?.tpin) return null; + const isMatch = await comparePassword(tpin, user.tpin); + return isMatch; +} + +async function setTpin(customerNo, tpin) { + const hashedTpin = await hashPassword(tpin); + try { + await db.query('UPDATE users SET tpin = $1 WHERE customer_no = $2', [ + hashedTpin, + customerNo, + ]); + } catch (error) { + throw new Error( + `error occured while while setting new tpin ${error.message}` + ); + } +} + +module.exports = { validateUser, findUserByCustomerNo, setTpin, validateTpin }; diff --git a/src/util/jwt.js b/src/util/jwt.js index 2135337..a5c6c90 100644 --- a/src/util/jwt.js +++ b/src/util/jwt.js @@ -1,12 +1,15 @@ const jwt = require('jsonwebtoken'); const { jwtSecret } = require('../config/config'); +const { logger } = require('./logger'); -function generateToken(payload, expiresIn = '1h') { - return jwt.sign({ payload }, jwtSecret, { expiresIn }); +function generateToken(customerNo, expiresIn = '1d') { + logger.info({ customerNo }, 'payload to encode'); + return jwt.sign({ customerNo }, jwtSecret, { expiresIn }); } function verifyToken(token) { - return jwt.verify(token, jwtSecret); + const payload = jwt.verify(token, jwtSecret); + return payload; } module.exports = { diff --git a/src/validators/tpin.validator.js b/src/validators/tpin.validator.js new file mode 100644 index 0000000..382f545 --- /dev/null +++ b/src/validators/tpin.validator.js @@ -0,0 +1,16 @@ +const authService = require('../services/auth.service'); + +const tpinValidator = async (req, res, next) => { + const customerNo = req.user; + const { tpin } = req.body; + + if (!tpin) { + return res.status(400).json({ error: 'BAD_REQUEST' }); + } + const valid = await authService.validateTpin(customerNo, tpin); + if (valid === null) res.status(400).json({ error: 'TPIN_NOT_SET_FOR_USER' }); + if (!valid) return res.status(401).json({ error: 'INCORRECT_TPIN' }); + next(); +}; + +module.exports = tpinValidator; diff --git a/src/validators/transfer.validator.js b/src/validators/transfer.validator.js new file mode 100644 index 0000000..dfb9f61 --- /dev/null +++ b/src/validators/transfer.validator.js @@ -0,0 +1,20 @@ +const transferValidator = async (req, res, next) => { + const { fromAccount, toAccount, toAccountType, amount } = req.body; + + const accountTypes = ['SB', 'LN']; + if (!fromAccount || fromAccount.length != 11) { + return res.status(400).json({ error: 'INVALID_ACCOUNT_NUMBER_FORMAT' }); + } + if (!toAccount || toAccount.length != 11) { + return res.status(400).json({ error: 'INVALID_ACCOUNT_NUMBER_FORMAT' }); + } + if (!accountTypes || !accountTypes.includes(toAccountType)) { + return res.status(400).json({ error: 'INVALID_ACCOUNT_TYPE' }); + } + if (!amount || amount <= 0) { + return res.status(400).json({ error: 'INVALID_AMOUNT' }); + } + next(); +}; + +module.exports = transferValidator;