Skip to content

Latest commit

Β 

History

11 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Bank Ledger System

A backend API for a banking ledger system built with Node.js, Express, and MongoDB. It handles user authentication, bank accounts, and money transfers using a proper double-entry bookkeeping model β€” meaning every transaction creates both a debit and a credit ledger entry, so the books always balance.

Every rupee amount is stored as integer paise internally (1 rupee = 100 paise). This avoids the IEEE 754 floating-point problem where 0.1 + 0.2 !== 0.3 β€” a real issue that compounds over time in financial systems.

Auth uses a dual-token system: short-lived 15-minute access tokens paired with 7-day refresh tokens. If your access token expires, you hit /auth/refresh β€” no re-login needed. The refresh token is stored as a SHA-256 hash in MongoDB, so even a DB breach doesn't hand over usable tokens.


What's Inside

bank-ledger/
β”œβ”€β”€ server.js                          # Entry point β€” boots the app, process guards
β”œβ”€β”€ jest.config.js                     # Jest test configuration
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ app.js                         # Express, middleware stack, /api/v1/ routes
β”‚   β”œβ”€β”€ config/
β”‚   β”‚   β”œβ”€β”€ db.js                      # MongoDB connection
β”‚   β”‚   └── validateEnv.js             # Fail-fast env variable validation
β”‚   β”œβ”€β”€ controllers/
β”‚   β”‚   β”œβ”€β”€ auth.controller.js         # Register, login, refresh, logout
β”‚   β”‚   β”œβ”€β”€ account.controller.js      # Create & fetch accounts, balance
β”‚   β”‚   └── transaction.controller.js  # Transfer, history, detail, initial-funds
β”‚   β”œβ”€β”€ middleware/
β”‚   β”‚   β”œβ”€β”€ auth.middleware.js         # JWT verification + role guard
β”‚   β”‚   β”œβ”€β”€ validate.middleware.js     # Input validation + rupeesβ†’paise conversion
β”‚   β”‚   β”œβ”€β”€ errorHandler.middleware.js # Central error handler
β”‚   β”‚   └── requestId.middleware.js    # X-Request-ID correlation headers
β”‚   β”œβ”€β”€ models/
β”‚   β”‚   β”œβ”€β”€ user.model.js              # User schema + bcrypt hooks
β”‚   β”‚   β”œβ”€β”€ account.model.js           # Bank account + getBalance() aggregation
β”‚   β”‚   β”œβ”€β”€ ledger.model.js            # Immutable DEBIT/CREDIT entries (paise)
β”‚   β”‚   β”œβ”€β”€ transaction.model.js       # Transaction records (paise amounts)
β”‚   β”‚   β”œβ”€β”€ refreshToken.model.js      # Hashed refresh tokens with 7d TTL
β”‚   β”‚   β”œβ”€β”€ blackList.model.js         # JWT blacklist (TTL-indexed, 15min)
β”‚   β”‚   └── auditLog.model.js          # Immutable audit trail (90d TTL)
β”‚   β”œβ”€β”€ routes/
β”‚   β”‚   β”œβ”€β”€ auth.routes.js
β”‚   β”‚   β”œβ”€β”€ accounts.routes.js
β”‚   β”‚   └── transaction.routes.js
β”‚   β”œβ”€β”€ services/
β”‚   β”‚   └── email.service.js           # Nodemailer with Google OAuth2
β”‚   └── utils/
β”‚       β”œβ”€β”€ asyncHandler.js            # Wraps async controllers for error propagation
β”‚       β”œβ”€β”€ audit.js                   # Fire-and-forget audit log helper
β”‚       β”œβ”€β”€ currency.js                # rupeesToPaise / paiseToRupees / formatRupees
β”‚       └── logger.js                  # Winston structured logger
└── tests/
    └── unit/
        β”œβ”€β”€ asyncHandler.test.js       # 5 tests
        β”œβ”€β”€ currency.test.js           # 12 tests (including IEEE 754 edge cases)
        └── validateEnv.test.js        # 7 tests

Tech Stack

  • Runtime: Node.js
  • Framework: Express v5
  • Database: MongoDB via Mongoose (requires replica set for ACID transactions)
  • Auth: Dual-token JWT β€” 15min access token + 7d refresh token (httpOnly cookies)
  • Password hashing: bcrypt (10 rounds)
  • Email: Nodemailer with Google OAuth2
  • Logging: Winston (structured JSON in prod) + Morgan (HTTP access logs)
  • Security: Helmet, CORS, express-rate-limit
  • Validation: express-validator
  • Testing: Jest + supertest

Getting Started

Prerequisites

  • Node.js 18+
  • A MongoDB instance with replica set enabled (needed for ACID transactions). MongoDB Atlas free tier works perfectly.
  • Gmail OAuth2 credentials for emails (optional β€” server works fine without it)

1. Clone and install

git clone https://github.com/Suke2004/Bank-Transaction-System.git
cd bank-ledger
npm install

2. Set up environment variables

cp .env.example .env

Then fill in your values. Generate secrets with:

node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"

Note: The server refuses to start if JWT_SECRET or REFRESH_TOKEN_SECRET are shorter than 32 characters. You need two separate secrets β€” one for each token type.

3. Run in development

npm run dev

4. Run tests

npm test                # run once with coverage
npm run test:watch      # watch mode during development

5. Check it's alive

GET http://localhost:3000/health

API Reference

Base URL: http://localhost:3000/api/v1

Auth: Protected routes require either:

  • A cookie named token (set automatically on login/register), or
  • Authorization: Bearer <token> header

Amount fields: Send amounts in rupees (e.g. 100.50). The API stores them as paise internally and returns rupees in all responses. Max 2 decimal places accepted.


Auth

Register

POST /api/v1/auth/register

Body:

{
  "name": "Harry Potter",
  "email": "harry@hogwarts.com",
  "password": "Expelliarmus1"
}

Response 201:

{
  "user": { "_id": "...", "email": "harry@hogwarts.com", "name": "Harry Potter" },
  "token": "<15min-access-jwt>"
}

Also sets two httpOnly cookies: token (15min) and refreshToken (7 days).

Validation: email must be valid, name 2–100 chars, password min 6 chars with at least one number.


Login

POST /api/v1/auth/login

Body:

{ "email": "harry@hogwarts.com", "password": "Expelliarmus1" }

Response 200:

{
  "user": { "_id": "...", "email": "harry@hogwarts.com", "name": "Harry Potter" },
  "token": "<15min-access-jwt>"
}

Sets both token and refreshToken cookies.

Same error message for wrong email vs wrong password β€” intentional, prevents user enumeration.


Refresh Access Token

POST /api/v1/auth/refresh

No body needed. Reads the refreshToken cookie automatically.

Response 200:

{ "token": "<new-15min-access-jwt>" }

Also sets a fresh token cookie. Call this when you get a 401 β€” no re-login needed.


Logout

POST /api/v1/auth/logout

πŸ”’ No body needed.

Blacklists the current access token + deletes the refresh token from DB + clears both cookies.

Response 200:

{ "message": "User logged out successfully" }

Accounts

Create a bank account

POST /api/v1/accounts

πŸ”’ Protected. Creates a new INR account. A user can have multiple accounts.

Response 201:

{
  "account": {
    "_id": "...",
    "user": "<userId>",
    "status": "ACTIVE",
    "currency": "INR",
    "createdAt": "..."
  }
}

List your accounts

GET /api/v1/accounts?page=1&limit=20

πŸ”’ Protected.

Query param Default Max
page 1 β€”
limit 20 50

Response 200:

{
  "accounts": [...],
  "pagination": { "page": 1, "limit": 20, "total": 3, "pages": 1 }
}

Get account balance

GET /api/v1/accounts/balance/:accountId

πŸ”’ Protected. Balance is derived live from the ledger (sum of CREDITs minus DEBITs in paise, returned in rupees). Always accurate.

Response 200:

{
  "accountId": "...",
  "balance": 100.50,
  "currency": "INR"
}

Transactions

Transfer money

POST /api/v1/transaction

πŸ”’ Protected.

Body:

{
  "fromAccount": "<accountId>",
  "toAccount": "<accountId>",
  "amount": 100.50,
  "idempotencyKey": "uuid-or-unique-string-per-attempt"
}

Response 201:

{
  "message": "Transaction completed successfully",
  "transaction": {
    "_id": "...",
    "fromAccount": "...",
    "toAccount": "...",
    "amount": 100.50,
    "status": "COMPLETED",
    "idempotencyKey": "..."
  }
}

About idempotencyKey: Generate a UUID per transfer attempt. If the same key is submitted twice (e.g. network retry), the second request returns the original result β€” no double-charge.

Validation: both IDs must be valid ObjectIds, amount > 0, max 2 decimal places, fromAccount β‰  toAccount.


Transaction history

GET /api/v1/transaction?page=1&limit=20&status=COMPLETED

πŸ”’ Protected. Returns all transactions where any of your accounts was sender or receiver.

Query param Default Options
page 1 β€”
limit 20 (max 50) β€”
status all PENDING, COMPLETED, FAILED, REVERSED

Response 200:

{
  "transactions": [
    {
      "_id": "...",
      "fromAccount": { "_id": "...", "currency": "INR", "status": "ACTIVE" },
      "toAccount": { "_id": "...", "currency": "INR", "status": "ACTIVE" },
      "amount": 100.50,
      "status": "COMPLETED",
      "createdAt": "..."
    }
  ],
  "pagination": { "page": 1, "limit": 20, "total": 5, "pages": 1 }
}

Single transaction

GET /api/v1/transaction/:id

πŸ”’ Protected. Returns 404 if the transaction doesn't involve your account.

Response 200:

{
  "transaction": { ... }
}

Fund an account (system user only)

POST /api/v1/transaction/system/initial-funds

πŸ”’ Requires systemUser flag. Admin top-up endpoint.

Body:

{
  "toAccount": "<accountId>",
  "amount": 10000,
  "idempotencyKey": "initial-funding-<userId>"
}

Health Check

GET /health

No auth. Returns server uptime. Used by load balancers and Kubernetes liveness probes.

Response 200:

{ "status": "ok", "timestamp": "2026-06-09T17:19:00.000Z", "uptime": 342.5 }

Error Responses

All errors follow this shape:

{ "status": "error", "message": "Human readable message" }

Validation errors include field-level detail:

{
  "status": "error",
  "message": "Validation failed",
  "errors": [
    { "field": "amount", "message": "amount must be a positive number greater than 0" }
  ]
}
Code Meaning
200 Success
201 Created
400 Bad request (business logic)
401 Not authenticated
403 Authenticated but not authorised
404 Not found
409 Conflict (duplicate key)
422 Validation failed
429 Rate limited
500 Server error

Rate Limits

Scope Limit
All endpoints 200 requests / 15 min / IP
/api/v1/auth/* 20 requests / 15 min / IP

Security

  • Passwords: bcrypt, 10 rounds, never stored in plain text
  • Access tokens: 15-minute JWT in httpOnly; Secure; SameSite=Strict cookie
  • Refresh tokens: 7-day random token stored hashed (SHA-256) in MongoDB
  • Logged-out tokens: blacklisted + auto-expire via TTL index
  • Security headers: Helmet (14 headers including CSP, X-Frame-Options)
  • Stack traces: never sent to clients in production

Audit Trail

Every sensitive action is written to an immutable auditLog collection:

Action Trigger
USER_REGISTER New user created
USER_LOGIN Successful login
USER_LOGOUT Logout called
TOKEN_REFRESH Access token refreshed
ACCOUNT_CREATED New bank account opened
TRANSACTION_INITIATED Transfer started
TRANSACTION_COMPLETED Transfer committed
TRANSACTION_FAILED Transfer rolled back
INITIAL_FUNDS_ADDED System user top-up

Audit entries store: userId, action, metadata, IP address, user agent, and X-Request-ID for end-to-end tracing. Auto-deleted after 90 days (configurable via AUDIT_LOG_RETENTION_DAYS).


Running Tests

npm test              # jest --coverage (23 tests)
npm run test:watch    # watch mode

Test coverage of utilities (100%):

  • asyncHandler.js β€” error forwarding, pass-through, error identity
  • currency.js β€” paise conversion, IEEE 754 edge cases, formatting
  • validateEnv.js β€” missing vars, weak secrets, mocked process.exit

License

ISC

About

This project demonstrate how a real-world banking backend system is designed and built using Node.js and Express. The project covers authentication, account management, transactions, ledger handling, and backend architecture.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages