Complete
Table of Contents
- Overview
- Architecture
- Environment Setup
- Project Structure
- Database Schema
- Authentication System
- API Endpoints
- Middleware
- Utilities
- Socket.io Events
- Scheduled Jobs
- Error Handling
- Development Guide
- Deployment
Overview
UserState API is a robust Node.js backend framework designed for managing user authentication, user profiles, payments, and referral systems. Built with Express.js, it provides a comprehensive set of features for modern web applications.
Key Capabilities
- Multi-strategy authentication (Local, JWT, Basic)
- User registration with OTP/Email verification
- Payment processing via Xendit
- Real-time payment status updates
- Referral and point system
- Address management
- Marketing offers and voucher redemption
- Push notifications via Firebase Cloud Messaging
Architecture
Technology Stack
| Component | Technology |
|---|---|
| Runtime | Node.js |
| Web Framework | Express.js |
| Primary Database | PostgreSQL (via Sequelize ORM) |
| Secondary Database | MongoDB (via Mongoose) |
| Cache/Session Store | Redis |
| Authentication | Passport.js |
| Validation | AJV, Express Validator |
| Security | Helmet, bcrypt, argon2 |
| Real-time | Socket.io |
| Payment Gateway | Xendit |
| Email Service | Nodemailer |
| Push Notifications | Firebase Admin SDK |
| Task Scheduling | node-cron |
Application Flow
Request → Express Middleware → Passport Auth → Route Handler → Controller → Model → Database
↓
Response/Error Handler
Environment Setup
Environment Variables
Create a .env file in the root directory with the following variables:
# Server Configuration
API_PORT=33010
ORIGIN_CORS=http://localhost:3000,http://example.com
# PostgreSQL Configuration
PS_HOST=localhost
PS_USER=postgres
PS_PASSWORD=your_password
PS_DB=userstate
PS_DIALECT=postgres
PS_PORT=5432
PS_POOL_MAX=50
# MongoDB Configuration
MONGO_DB=mongodb://localhost:27017/userstate
# Redis Configuration
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=your_redis_password
# Authentication
SECRET_AUTH_CONFIG=your_jwt_secret_key
SANDBOX_AUTH_CONFIG=your_sandbox_key
# Xendit Configuration
XENDIT_SECRET_KEY=your_xendit_secret_key
XENDIT_PUBLIC_KEY=your_xendit_public_key
XENDIT_CALLBACK_TOKEN=your_callback_token
# External APIs
API_URL_SUNSHINE=https://sunshine-api-url
API_URL_SMSVIRO=https://smsviro-api-url
SMSVIROTOKEN=your_sms_token
# Email Configuration (Nodemailer)
EMAIL_HOST=smtp.hostinger.com
EMAIL_PORT=465
EMAIL_USER=noreply@yourdomain.com
EMAIL_PASS=your_email_password
# Firebase Configuration
# Place your Firebase service account JSON in keystore/onmapss-main-key.json
Installation Steps
-
Install Node.js Dependencies
npm install -
Setup PostgreSQL Database
createdb userstate -
Setup MongoDB (if using payment logs)
# Ensure MongoDB is running mongod --dbpath /path/to/data -
Setup Redis
# Ensure Redis is running redis-server -
Configure Firebase
- Download your Firebase service account key JSON
- Place it in
keystore/onmapss-main-key.json
-
Run Database Migrations
- The application uses auto-sync for Sequelize models
- Uncomment the sync lines in
index.jsif needed:
await db.user_login.sync({ alter: true }); await db.user_info.sync({ alter: true }); // ... etc
Project Structure
Directory Overview
userstate-api/
│
├── config/ # Configuration files
│ ├── api.js # External API configurations
│ ├── auth.config.js # JWT and auth secrets
│ ├── db.js # Database connection settings
│ ├── multer.js # File upload configuration
│ ├── pg.js # PostgreSQL initialization
│ ├── redis.js # Redis client setup
│ └── voucher-ext.json # Voucher extensions data
│
├── controllers/ # Business logic handlers
│ ├── address.controller.js # Address CRUD operations
│ ├── auth.controller.js # Authentication logic
│ ├── faq.controller.js # FAQ management
│ ├── fcm.controller.js # Firebase Cloud Messaging
│ ├── marketing_offer.controller.js # Marketing offers
│ ├── passport.controller.js # Passport strategy handlers
│ ├── payment.controller.js # Payment processing
│ ├── payout.controller.js # Payout operations
│ ├── point_mutation.controller.js # Points system
│ ├── referral.controller.js # Referral system
│ └── user_info.controller.js # User profile management
│
├── middleware/ # Custom middleware
│ ├── auth.js # Authentication middleware
│ ├── authJwt.js # JWT verification
│ ├── error_param_handler.js # Error handling
│ ├── express_validator.js # Express validator wrapper
│ ├── json_validator.js # AJV JSON schema validator
│ ├── other_validator.js # Custom validators
│ ├── passport.js # Passport strategies
│ ├── upload_update_profile.js # File upload middleware
│ └── verify.js # Token verification
│
├── models/ # Database models
│ ├── index.js # Model loader and relationships
│ ├── user_login.model.js # User credentials
│ ├── user_info.model.js # User profile information
│ ├── user_balance.model.js # User wallet balance
│ ├── address_list.model.js # User addresses
│ ├── point_mutation.model.js # Point transactions
│ ├── referral_history.model.js # Referral tracking
│ ├── marketing_offer.model.js # Offers
│ ├── fcm.model.js # FCM tokens
│ ├── api_key.model.js # API keys for services
│ ├── otp_tracker.model.js # OTP tracking
│ ├── bank_account.model.js # Bank account info
│ ├── bank_channel.model.js # Payment channels
│ ├── payment_type.model.js # Payment methods
│ ├── user_transaction.model.js # Transaction history
│ ├── voucher_redemption.model.js # Voucher usage
│ ├── deactivate_reason.model.js # Account deactivation
│ ├── xendit_payout_logs.model.js # Payout logs (MongoDB)
│ └── xendit_checkout_logs.model.js # Checkout logs (MongoDB)
│
├── routers/ # Route definitions
│ ├── address.route.js # Address endpoints
│ ├── auth.route.js # Authentication endpoints
│ ├── faq.route.js # FAQ endpoints
│ ├── fcm.route.js # FCM endpoints
│ ├── marketing_offer.route.js # Marketing endpoints
│ ├── payment.route.js # Payment endpoints
│ ├── point_mutation.route.js # Points endpoints
│ ├── referral.route.js # Referral endpoints
│ └── user_info.route.js # User profile endpoints
│
├── schemas/ # Request validation schemas
│ ├── index.js # Schema exporter
│ ├── address.schema.js # Address validation
│ ├── auth.schema.js # Auth validation
│ ├── fcm.schema.js # FCM validation
│ ├── marketing_offer.schema.js # Marketing validation
│ ├── payment.schema.js # Payment validation
│ ├── point_mutation.schema.js # Points validation
│ ├── referral.schema.js # Referral validation
│ └── user_info.schema.js # User profile validation
│
├── socket/ # Socket.io implementations
│ └── payment_status.socket.js # Real-time payment updates
│
├── scheduler/ # Cron job definitions
│ └── xendit.scheduler.js # Payment status sync
│
├── utilities/ # Utility functions
│ ├── encrypt.util.js # Encryption helpers
│ ├── general.util.js # General utilities
│ ├── hashing_center.util.js # Hashing functions
│ └── qrcode.util.js # QR code generation
│
├── libraries/ # Static data files
│ ├── dir_day.json # Day mappings
│ ├── dir_digits.json # Digit mappings
│ ├── dir_month.json # Month mappings
│ └── dir_year.json # Year mappings
│
├── keystore/ # Private keys and certificates
│ └── onmapss-main-key.json # Firebase service account key
│
├── resources/ # Generated files (gitignored)
│ └── user_profile_img/ # User profile images
│
├── index.js # Application entry point
├── utility.js # Shared utility functions
├── package.json # Dependencies and scripts
└── .gitignore # Git ignore rules
Database Schema
PostgreSQL Tables (via Sequelize)
1. user_login
Stores user authentication credentials.
| Column | Type | Description |
|---|---|---|
| username | STRING (PK) | Unique username |
| pwd | STRING | Hashed password |
| disabled | BOOLEAN | Account status |
| created_at | DATE | Registration date |
| updated_at | DATE | Last update |
2. user_info
Stores user profile information.
| Column | Type | Description |
|---|---|---|
| id | INTEGER (PK, Auto) | Primary key |
| username | STRING (FK) | Reference to user_login |
| full_name | STRING | User's full name |
| STRING (Unique) | Email address | |
| phone_number | STRING | Phone number |
| profile_img | STRING | Profile image path |
| agent | BOOLEAN | Agent status |
| referral_code | STRING | Unique referral code |
| created_at | DATE | Creation date |
| updated_at | DATE | Last update |
3. user_balance
Tracks user wallet balance.
| Column | Type | Description |
|---|---|---|
| username | STRING (PK, FK) | Reference to user_login |
| balance | DECIMAL | Current balance |
| points | INTEGER | Reward points |
| created_at | DATE | Creation date |
| updated_at | DATE | Last update |
4. address_list
Stores user addresses.
| Column | Type | Description |
|---|---|---|
| id | INTEGER (PK, Auto) | Primary key |
| username | STRING (FK) | Reference to user_login |
| label | STRING | Address label |
| recipient_name | STRING | Recipient name |
| phone_number | STRING | Contact number |
| address | TEXT | Full address |
| is_default | BOOLEAN | Default address flag |
| created_at | DATE | Creation date |
| updated_at | DATE | Last update |
5. point_mutation
Tracks point transactions.
| Column | Type | Description |
|---|---|---|
| id | INTEGER (PK, Auto) | Primary key |
| username | STRING (FK) | Reference to user_login |
| type | STRING | CREDIT/DEBIT |
| amount | INTEGER | Point amount |
| description | STRING | Transaction description |
| reference_id | STRING | External reference |
| created_at | DATE | Transaction date |
6. referral_history
Tracks referral relationships.
| Column | Type | Description |
|---|---|---|
| id | INTEGER (PK, Auto) | Primary key |
| username | STRING (FK) | Referred user |
| submitted_by | STRING (FK) | Referrer username |
| referral_code | STRING | Used referral code |
| status | STRING | Referral status |
| created_at | DATE | Creation date |
7. marketing_offer
Stores promotional offers.
| Column | Type | Description |
|---|---|---|
| id | INTEGER (PK, Auto) | Primary key |
| username | STRING (FK) | Target user (nullable) |
| title | STRING | Offer title |
| description | TEXT | Offer details |
| image_url | STRING | Offer image |
| valid_until | DATE | Expiration date |
| is_active | BOOLEAN | Active status |
| created_at | DATE | Creation date |
8. fcm
Stores Firebase Cloud Messaging tokens.
| Column | Type | Description |
|---|---|---|
| id | INTEGER (PK, Auto) | Primary key |
| username | STRING (FK) | Reference to user_login |
| token | TEXT | FCM device token |
| device_type | STRING | iOS/Android |
| created_at | DATE | Registration date |
9. user_transaction
Tracks payment transactions.
| Column | Type | Description |
|---|---|---|
| id | STRING (PK) | Transaction ID |
| username | STRING (FK) | Reference to user_login |
| type | STRING | Transaction type |
| amount | DECIMAL | Transaction amount |
| status | STRING | Transaction status |
| payment_method | STRING | Payment channel |
| reference_id | STRING | External reference |
| created_at | DATE | Transaction date |
10. voucher_redemption
Tracks voucher usage.
| Column | Type | Description |
|---|---|---|
| id | INTEGER (PK, Auto) | Primary key |
| username | STRING (FK) | Reference to user_login |
| voucher_code | STRING | Voucher code |
| discount_amount | DECIMAL | Discount value |
| redeemed_at | DATE | Redemption date |
11. api_key
Stores API keys for service authentication.
| Column | Type | Description |
|---|---|---|
| holder | STRING (PK) | Service identifier |
| key | STRING | Hashed API key |
| description | STRING | Key description |
| created_at | DATE | Creation date |
12. otp_tracker
Tracks OTP requests for rate limiting.
| Column | Type | Description |
|---|---|---|
| id | INTEGER (PK, Auto) | Primary key |
| identifier | STRING | Email/Phone |
| otp_type | STRING | OTP purpose |
| count | INTEGER | Request count |
| last_request | DATE | Last request time |
MongoDB Collections (via Mongoose)
xendit_payout_logs
Stores Xendit payout transaction logs.
{
payout_id: String,
username: String,
amount: Number,
status: String,
bank_code: String,
account_number: String,
account_holder_name: String,
description: String,
reference_id: String,
created_at: Date,
updated_at: Date
}
xendit_checkout_logs
Stores Xendit checkout/invoice logs.
{
invoice_id: String,
external_id: String,
username: String,
amount: Number,
status: String,
payment_method: String,
payment_channel: String,
description: String,
invoice_url: String,
expiry_date: Date,
created_at: Date,
updated_at: Date
}
Model Relationships
user_login
├── hasOne → user_info
├── hasOne → user_balance
├── hasMany → address_list
├── hasMany → marketing_offer
├── hasMany → fcm
├── hasMany → user_transaction
└── hasMany → voucher_redemption
user_info
├── belongsTo → user_login
├── belongsTo → user_balance
└── hasMany → referral_history (as referrer)
referral_history
├── belongsTo → user_info (username)
└── belongsTo → user_info (submitted_by as referrer)
Authentication System
The application uses Passport.js with three authentication strategies:
1. Local Strategy (Username/Password)
Used for: User login with username/email and password
Endpoint: POST /auth
Flow:
- User submits credentials
- System checks database for username or email
- If user not confirmed, checks Redis for temporary data
- Password is verified using bcrypt
- JWT token generated on success
Middleware: passport.PassportLocal
2. JWT Strategy (Bearer Token)
Used for: Protected route access
Endpoints: Most authenticated routes
Flow:
- Client sends JWT in Authorization header:
Bearer <token> - Token is decoded and validated
- User's password hash is used as secret key (dynamic per user)
- User information extracted from token payload
Middleware: passport.PassportJwt
Token Structure:
{
user: "username",
agent: true/false,
iat: 1234567890,
exp: 1234567890
}
3. Basic Strategy (API Key)
Used for: Service-to-service authentication
Endpoints: Internal/system endpoints (/internal/*, /sys/*)
Flow:
- Client sends Basic Auth header:
Basic base64(holder:apiKey) - System looks up API key in database
- API key verified using bcrypt
Middleware: passport.PassportBasic
Token Management
JWT Secret Key: Dynamically generated from user's password hash
- Formula:
pwd.slice(32, 48) + pwd.slice(14, 30) - Ensures tokens invalidate on password change
Token Expiration: Configurable via environment
- Session tokens can be extended via
/extendendpoint
Redis Storage: Used for temporary user data during registration
API Endpoints
Authentication Endpoints
POST /auth
Description: User login
Auth: Local Strategy
Body:
{
"username": "john_doe",
"password": "securePassword123"
}
Response:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"agent": false
}
POST /signup
Description: User registration with email/OTP verification
Auth: None
Body:
{
"username": "john_doe",
"password": "securePassword123",
"email": "john@example.com",
"full_name": "John Doe",
"phone_number": "628123456789",
"referral_code": "REF123",
"mode": "LINK"
}
Modes:
-
LINK: Email verification link -
OTP: SMS OTP verification
POST /check/email
Description: Check if email is already registered
Auth: None
Body:
{
"email": "john@example.com"
}
POST /verify-sign-up
Description: Verify OTP or email verification
Auth: JWT Token
Body:
{
"otp": "123456"
}
POST /request-reset-pwd
Description: Request password reset
Auth: None
Body:
{
"email": "john@example.com"
}
POST /reset-pwd
Description: Reset password with token
Auth: Token from email
Body:
{
"password": "newSecurePassword123"
}
POST /update-pwd
Description: Update password (logged in)
Auth: JWT
Body:
{
"old_password": "oldPassword123",
"new_password": "newPassword123"
}
GET /extend
Description: Extend JWT token expiration
Auth: JWT
Response:
{
"token": "new_jwt_token..."
}
GET /verify
Description: Verify if JWT token is valid
Auth: JWT
Response: 200 OK or 401 Unauthorized
GET /logoff
Description: Logout user
Auth: JWT
Response: 200 OK
POST /deactivate
Description: Deactivate user account
Auth: JWT
Body:
{
"reason": "No longer needed"
}
User Profile Endpoints
GET /user/profile
Description: Get user profile information
Auth: JWT
Response:
{
"username": "john_doe",
"full_name": "John Doe",
"email": "john@example.com",
"phone_number": "628123456789",
"profile_img": "/resources/user_profile_img/john_doe.jpg",
"referral_code": "JOHN123",
"balance": 50000,
"points": 150
}
PUT /user/profile
Description: Update user profile
Auth: JWT
Body:
{
"full_name": "John Updated",
"phone_number": "628987654321"
}
POST /user/update-profile-img
Description: Upload profile image
Auth: JWT
Content-Type: multipart/form-data
Body: Form data with profile_img file
GET /user/referral-code
Description: Get user's referral code
Auth: JWT
Response:
{
"referral_code": "JOHN123"
}
Address Endpoints
GET /address/list
Description: Get all user addresses
Auth: JWT
Response:
[
{
"id": 1,
"label": "Home",
"recipient_name": "John Doe",
"phone_number": "628123456789",
"address": "Jl. Example No. 123",
"is_default": true
}
]
POST /address
Description: Create new address
Auth: JWT
Body:
{
"label": "Office",
"recipient_name": "John Doe",
"phone_number": "628123456789",
"address": "Jl. Office No. 456",
"is_default": false
}
PUT /address/:id
Description: Update address
Auth: JWT
Body: Same as create
DELETE /address/:id
Description: Delete address
Auth: JWT
POST /address/set-default
Description: Set default address
Auth: JWT
Body:
{
"id": 1
}
Payment Endpoints
POST /payment-checkout
Description: Create payment checkout/invoice
Auth: JWT
Body:
{
"amount": 100000,
"description": "Order #12345",
"payment_method": "EWALLET",
"ewallet_type": "OVO"
}
POST /sys/payment-checkout
Description: System checkout (for internal services)
Auth: Basic
Body:
{
"username": "john_doe",
"amount": 100000,
"description": "Order #12345"
}
POST /payment-callback
Description: Xendit payment callback
Auth: Callback Token
Body: Xendit callback payload
GET /balance
Description: Get user balance
Auth: JWT
Response:
{
"balance": 50000,
"points": 150
}
POST /balance/decrease
Description: Decrease user balance
Auth: JWT
Body:
{
"amount": 10000,
"description": "Purchase payment"
}
GET /invoice/detail
Description: Get invoice details
Auth: None (query param based)
Query: ?invoice_id=inv_123
GET /invoice/detail/xendit
Description: Get Xendit invoice details
Auth: JWT
Query: ?invoice_id=inv_123
GET /invoice/list/xendit
Description: List user's Xendit invoices
Auth: JWT
POST /invoice/cancel/xendit
Description: Cancel Xendit invoice
Auth: JWT
Body:
{
"invoice_id": "inv_123"
}
POST /redeem-voucher
Description: Redeem voucher code
Auth: Basic
Body:
{
"username": "john_doe",
"voucher_code": "PROMO123"
}
GET /check-voucher-redemption
Description: Check if user has redeemed voucher
Auth: JWT
Query: ?voucher_code=PROMO123
POST /bank/list
Description: Get list of available bank codes
Auth: None
Body:
{
"type": "DISBURSEMENT"
}
GET /payment/list
Description: Get available payment types
Auth: None
Point & Referral Endpoints
GET /points/history
Description: Get point transaction history
Auth: JWT
Response:
[
{
"id": 1,
"type": "CREDIT",
"amount": 50,
"description": "Referral bonus",
"created_at": "2024-01-01T00:00:00.000Z"
}
]
POST /points/redeem
Description: Redeem points for balance
Auth: JWT
Body:
{
"points": 100
}
GET /referral/history
Description: Get referral history
Auth: JWT
Response:
[
{
"username": "referred_user",
"referral_code": "JOHN123",
"status": "completed",
"created_at": "2024-01-01T00:00:00.000Z"
}
]
GET /referral/stats
Description: Get referral statistics
Auth: JWT
Response:
{
"total_referrals": 10,
"completed_referrals": 8,
"pending_referrals": 2,
"total_earned_points": 400
}
FCM Endpoints
POST /fcm/register
Description: Register FCM device token
Auth: JWT
Body:
{
"token": "fcm_device_token...",
"device_type": "android"
}
DELETE /fcm/token
Description: Remove FCM token
Auth: JWT
Query: ?token=fcm_device_token...
POST /fcm/send
Description: Send push notification (admin)
Auth: Basic
Body:
{
"username": "john_doe",
"title": "New Offer",
"body": "Check out our latest promotion!",
"data": {
"offer_id": "123"
}
}
Marketing Offer Endpoints
GET /marketing-offer/list
Description: Get available marketing offers
Auth: JWT
Response:
[
{
"id": 1,
"title": "Summer Sale",
"description": "Get 50% off!",
"image_url": "/images/offer1.jpg",
"valid_until": "2024-12-31T23:59:59.000Z",
"is_active": true
}
]
POST /marketing-offer
Description: Create marketing offer (admin)
Auth: Basic
Body:
{
"title": "New Year Sale",
"description": "Special discount",
"image_url": "/images/offer.jpg",
"valid_until": "2024-12-31"
}
Middleware
Authentication Middleware
passport.PassportLocal
- Validates username and password
- Returns JWT token on success
- Used in:
/auth
passport.PassportJwt
- Validates Bearer token
- Extracts user from token
- Dynamic secret key per user
- Used in: Most protected routes
passport.PassportBasic
- Validates HTTP Basic Auth
- Checks API key against database
- Used in:
/internal/*,/sys/*routes
authJwt.verifyToken
- Custom JWT verification
- Used for specific token validation scenarios
- Checks token from Authorization header or query params
Validation Middleware
validator.validate({ body: schema })
- Uses AJV to validate request body against JSON schema
- Automatically returns 400 with validation errors
- Schemas defined in
schemas/directory
contentTypeValid(type)
- Ensures request Content-Type matches expected type
- Example:
contentTypeValid("application/json")
verifyToken(type)
- Validates special tokens (REGISTER, RESET_PWD)
- Checks token from query params or body
- Validates against Redis storage
verifyCallbackToken
- Validates Xendit callback tokens
- Ensures callbacks are from legitimate source
Error Handling Middleware
errorHandlerParam.JsonHandler
- Catches JSON parsing errors
- Returns proper error response
errorHandlerParam.otherHandler
- Global error handler
- Formats error responses
- Logs errors for debugging
Upload Middleware
upload_update_profile
- Handles profile image uploads
- Uses multer for multipart/form-data
- Validates file type and size
- Stores in
/resources/user_profile_img/
Utilities
utility.js
provideKey(req, rawJwtToken, done)
- Provides secret key for JWT verification
- Extracts key from user's password hash
- Ensures tokens invalidate on password change
admin()
- Initializes Firebase Admin SDK
- Returns admin instance for FCM
sendMail(to, subject, text, html)
- Sends email via Nodemailer
- Uses configured SMTP settings
- Supports HTML templates
generateRandomString(length, numberOnly)
- Generates random strings or numbers
- Used for OTP generation
syncInitBalance()
- Initializes user balance records
- Utility function for data migration
Encryption Utilities (utilities/encrypt.util.js)
- Encryption and decryption helpers
- Used for sensitive data
Hashing Utilities (utilities/hashing_center.util.js)
- Centralized hashing functions
- bcrypt and argon2 wrappers
QR Code Utilities (utilities/qrcode.util.js)
- QR code generation
- Used for payment or referral codes
General Utilities (utilities/general.util.js)
- Common helper functions
- Data formatting and transformation
Socket.io Events
Payment Status Socket
Namespace: Default namespace
File: socket/payment_status.socket.js
Server Events
payment:status:update
Description: Broadcast payment status update to specific user
Payload:
{
invoice_id: "inv_123",
status: "PAID",
amount: 100000,
paid_at: "2024-01-01T00:00:00.000Z"
}
Client Events
subscribe:payment
Description: Subscribe to payment updates for user
Payload:
{
username: "john_doe"
}
unsubscribe:payment
Description: Unsubscribe from payment updates
Payload:
{
username: "john_doe"
}
Usage Example
Client-side (JavaScript):
const socket = io('http://localhost:33010');
// Subscribe to payment updates
socket.emit('subscribe:payment', { username: 'john_doe' });
// Listen for payment status updates
socket.on('payment:status:update', (data) => {
console.log('Payment updated:', data);
// Update UI accordingly
});
// Unsubscribe when done
socket.emit('unsubscribe:payment', { username: 'john_doe' });
Scheduled Jobs
Xendit Payment Status Sync
File: scheduler/xendit.scheduler.js
Schedule: Every 2 hours (0 */2 * * *)
Function: expireStatusUpdater
Purpose:
- Synchronizes payment status with Xendit
- Updates expired invoices
- Notifies users of status changes via Socket.io
Manual Trigger: Uncomment endpoint in index.js
app.get("/scheduler", xenditScheduler.expireStatusUpdater);
Implementation Details:
- Fetches pending/processing invoices from database
- Queries Xendit API for current status
- Updates local database with latest status
- Emits Socket.io events for status changes
- Logs results to MongoDB
Error Handling
Error Response Format
All errors follow a consistent format:
{
"status": false,
"message": "Error description",
"errorCode": 400,
"errors": [] // Optional validation errors
}
HTTP Status Codes
| Code | Description | Usage |
|---|---|---|
| 200 | OK | Successful request |
| 201 | Created | Resource created successfully |
| 400 | Bad Request | Invalid request data |
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Authenticated but not permitted |
| 404 | Not Found | Resource not found |
| 409 | Conflict | Duplicate resource (e.g., email exists) |
| 422 | Unprocessable Entity | Invalid credentials |
| 500 | Internal Server Error | Server error |
Common Error Messages
Authentication Errors
-
"Invalid Credential"(422): Wrong username/password -
"Account does not exist"(500): User not found -
"Account is Disabled"(403): Account deactivated -
"OTP Not Confirmed"(403): Registration pending OTP -
"Account is Not Confirmed"(403): Registration pending email
Validation Errors
-
"Email is already registered"(409): Duplicate email -
"PhoneRequired": Phone number required for OTP mode -
"PhoneFormatErr": Invalid phone number format
Payment Errors
-
"Insufficient Balance": Not enough funds -
"Invoice Not Found": Invalid invoice ID -
"Invoice Already Paid": Cannot modify paid invoice
Error Handler Middleware
JsonHandler: Catches JSON parsing errors
app.use(errorHandlerParam.JsonHandler);
otherHandler: Global error handler
app.use(errorHandlerParam.otherHandler);
Logging
The application uses morgan with custom colored format for request logging:
- Method: Blue
- URL: Green
- Status: Yellow
- Response Time: Cyan
Errors are logged to console with stack traces in development.
Development Guide
Getting Started
-
Clone and Install
git clone <repo-url> cd userstate-api npm install -
Configure Environment
cp .env.example .env # Edit .env with your configuration -
Start Development Server
npm startUses nodemon for auto-reload on file changes.
Project Conventions
Code Style
- Use ES6+ features
- Async/await preferred over callbacks
- Use destructuring for cleaner code
- Follow existing file naming patterns
File Naming
- Models:
*.model.js - Controllers:
*.controller.js - Routes:
*.route.js - Schemas:
*.schema.js - Middleware: descriptive names with context
Database Conventions
- Use Sequelize for PostgreSQL operations
- Use Mongoose for MongoDB operations
- Always use transactions for critical operations
- Define model relationships in
models/index.js
Security Best Practices
- Always hash passwords with bcrypt/argon2
- Validate all user input
- Use parameterized queries (ORM handles this)
- Sanitize file uploads
- Implement rate limiting for sensitive endpoints
- Never log sensitive data (passwords, tokens, keys)
Adding New Features
1. Adding a New Endpoint
Step 1: Create Controller (controllers/feature.controller.js)
const db = require("../models");
exports.myEndpoint = async (req, res) => {
try {
// Your logic here
res.status(200).json({ success: true });
} catch (err) {
res.status(500).json({ message: err.message });
}
};
Step 2: Create Schema (schemas/feature.schema.js)
exports.mySchema = {
type: "object",
properties: {
field1: { type: "string" },
field2: { type: "number" }
},
required: ["field1"]
};
Step 3: Create Route (routers/feature.route.js)
const controller = require("../controllers/feature.controller");
const passport = require("../controllers/passport.controller");
const { validator, feature } = require("../schemas");
module.exports = (app) => {
app.post(
"/feature/endpoint",
[
passport.PassportJwt,
validator.validate({ body: feature.mySchema })
],
controller.myEndpoint
);
};
Step 4: Load Route in index.js
require("./routers/feature.route")(app);
2. Adding a New Model
Step 1: Create Model File (models/my_table.model.js)
module.exports = (sequelize, DataTypes) => {
const MyTable = sequelize.define("my_table", {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
field1: {
type: DataTypes.STRING,
allowNull: false
},
field2: {
type: DataTypes.TEXT,
allowNull: true
}
}, {
timestamps: true,
underscored: true
});
return MyTable;
};
Step 2: Load Model in models/index.js
db.my_table = require("./my_table.model")(
sequalize_config,
sequelize.DataTypes
);
Step 3: Define Relationships (if any)
db.user_login.hasMany(db.my_table, {
foreignKey: "username",
sourceKey: "username"
});
Testing
Manual Testing
Use tools like:
- Postman: For API testing
- curl: For command-line testing
- Socket.io Client: For WebSocket testing
Example curl Commands
Login:
curl -X POST http://localhost:33010/auth \
-H "Content-Type: application/json" \
-d '{"username":"test","password":"password123"}'
Get Profile (with JWT):
curl -X GET http://localhost:33010/user/profile \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
Basic Auth (Internal endpoints):
curl -X POST http://localhost:33010/sys/payment-checkout \
-H "Content-Type: application/json" \
-H "Authorization: Basic $(echo -n 'holder:apikey' | base64)" \
-d '{"username":"test","amount":10000}'
Debugging
Enable Sequelize Query Logging
In config/db.js:
const sequalize_config = new sequelize(
userstate.DB,
userstate.USER,
userstate.PASSWORD,
{
host: userstate.HOST,
dialect: userstate.DIALECT,
logging: console.log, // Add this line
// ...
}
);
Database Connection Issues
Check:
- PostgreSQL is running:
pg_isready - Credentials in
.envare correct - Database exists:
psql -l - Firewall allows connection
Redis Connection Issues
Check:
- Redis is running:
redis-cli ping - Redis configuration in
config/redis.js - Password authentication if enabled
Common Issues
"Cannot find module": Run npm install
"Port already in use": Change API_PORT in .env or kill process:
lsof -ti:33010 | xargs kill -9
"Connection refused": Database or Redis not running
Deployment
Production Checklist
- Set
NODE_ENV=productionin environment - Use strong, unique values for
SECRET_AUTH_CONFIG - Configure production database credentials
- Set up proper CORS origins
- Enable HTTPS/SSL
- Configure firewall rules
- Set up database backups
- Configure monitoring and logging
- Set up PM2 or similar process manager
- Remove or protect debug endpoints
Using PM2
Install PM2:
npm install -g pm2
Start Application:
pm2 start index.js --name userstate-api
PM2 Configuration (ecosystem.config.js):
module.exports = {
apps: [{
name: 'userstate-api',
script: './index.js',
instances: 2,
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z'
}]
};
Start with config:
pm2 start ecosystem.config.js
Useful PM2 Commands:
pm2 list # List all processes
pm2 logs userstate-api # View logs
pm2 restart userstate-api # Restart app
pm2 stop userstate-api # Stop app
pm2 delete userstate-api # Remove from PM2
pm2 monit # Monitor resources
Nginx Reverse Proxy
Example Nginx Configuration:
server {
listen 80;
server_name api.yourdomain.com;
location / {
proxy_pass http://localhost:33010;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# WebSocket support for Socket.io
location /socket.io/ {
proxy_pass http://localhost:33010;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
Docker Deployment
Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 33010
CMD ["node", "index.js"]
docker-compose.yml:
version: '3.8'
services:
api:
build: .
ports:
- "33010:33010"
environment:
- NODE_ENV=production
env_file:
- .env
depends_on:
- postgres
- redis
- mongo
restart: unless-stopped
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: userstate
POSTGRES_USER: postgres
POSTGRES_PASSWORD: your_password
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
redis:
image: redis:7-alpine
command: redis-server --requirepass your_redis_password
volumes:
- redis_data:/data
restart: unless-stopped
mongo:
image: mongo:6
volumes:
- mongo_data:/data/db
restart: unless-stopped
volumes:
postgres_data:
redis_data:
mongo_data:
Environment Variables for Production
# Production Settings
NODE_ENV=production
API_PORT=33010
# Use production database
PS_HOST=your-prod-db-host
PS_USER=prod_user
PS_PASSWORD=strong_password
PS_DB=userstate_prod
PS_DIALECT=postgres
PS_PORT=5432
PS_POOL_MAX=50
# Strong JWT secret
SECRET_AUTH_CONFIG=long_random_string_min_32_chars
# Production CORS
ORIGIN_CORS=https://app.yourdomain.com,https://www.yourdomain.com
# External services
XENDIT_SECRET_KEY=xnd_production_...
Monitoring
Health Check Endpoint:
Add to index.js:
app.get("/health", (req, res) => {
res.json({
status: "OK",
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
PM2 Monitoring:
pm2 install pm2-logrotate # Log rotation
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 7
Backup Strategy
Database Backup (PostgreSQL):
# Daily backup script
pg_dump -U postgres -h localhost userstate_prod > backup_$(date +%Y%m%d).sql
# Automated daily backup with cron
0 2 * * * /path/to/backup_script.sh
Redis Backup:
Redis automatically saves RDB snapshots. Configure in redis.conf:
save 900 1
save 300 10
save 60 10000
Conclusion
This documentation provides a comprehensive guide to the UserState API. For questions or issues, please contact the development team or create an issue in the repository.
Quick Links
- Repository: [Your repository URL]
-
API Base URL:
http://localhost:33010(development) - Socket.io: Same as API base URL
Support
For support and questions:
- Email: support@yourdomain.com
- Documentation updates: Create a pull request
- Bug reports: Open an issue
Last Updated: February 2024
Version: 1.0.0
Author: Development Team