Production-ready Node.js/Express backend for crypto trading bot SaaS
sentimint-backend/ ├── config/ # Configuration files │ ├── database.js # Database connection │ ├── jwt.js # JWT configuration │ └── websocket.js # WebSocket configuration ├── controllers/ # Route controllers │ ├── auth.controller.js │ ├── bot.controller.js │ ├── payment.controller.js │ ├── affiliate.controller.js │ └── user.controller.js ├── models/ # Database models │ ├── User.js │ ├── Subscription.js │ ├── Trade.js │ ├── Affiliate.js │ └── Queue.js ├── middleware/ # Custom middleware │ ├── auth.js │ ├── error.js │ └── validation.js ├── routes/ # API routes │ ├── auth.routes.js │ ├── bot.routes.js │ ├── payment.routes.js │ ├── affiliate.routes.js │ └── user.routes.js ├── services/ # Business logic │ ├── auth.service.js │ ├── bot.service.js │ ├── payment.service.js │ ├── affiliate.service.js │ ├── queue.service.js │ └── websocket.service.js ├── utils/ # Utility functions │ ├── logger.js │ ├── helpers.js │ └── validators.js ├── queues/ # Bull queue processors │ └── trading.queue.js ├── app.js # Express app setup ├── server.js # Server entry point └── .env # Environment variables
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const UserSchema = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true
},
password: {
type: String,
required: true,
minlength: 8
},
isVerified: {
type: Boolean,
default: false
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
tradingEnabled: {
type: Boolean,
default: false
},
apiKey: {
type: String,
default: ''
},
apiSecret: {
type: String,
default: ''
},
affiliateCode: {
type: String,
unique: true
},
referredBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
resetPasswordToken: String,
resetPasswordExpire: Date,
emailVerificationToken: String,
emailVerificationExpire: Date,
lastLogin: Date,
loginHistory: [{
ip: String,
device: String,
timestamp: Date
}]
}, {
timestamps: true
});
// Hash password before saving
UserSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// Generate affiliate code if not exists
UserSchema.pre('save', function(next) {
if (!this.affiliateCode) {
this.affiliateCode = Math.random().toString(36).substring(2, 8) +
Math.random().toString(36).substring(2, 8);
}
next();
});
// Method to compare passwords
UserSchema.methods.matchPassword = async function(enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
module.exports = mongoose.model('User', UserSchema);
const mongoose = require('mongoose');
const SubscriptionSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
lemonSqueezyId: {
type: String,
required: true
},
orderId: {
type: String,
required: true
},
productId: {
type: String,
required: true,
enum: ['basic', 'pro', 'enterprise'] // Corresponds to $99/$299/$999 tiers
},
status: {
type: String,
enum: ['active', 'expired', 'cancelled', 'pending'],
default: 'pending'
},
currentPeriodEnd: Date,
renewsAt: Date,
trialEndsAt: Date,
isUsageBased: {
type: Boolean,
default: false
},
subscriptionItemId: String,
variantId: String,
paymentMethod: String,
billingAnchor: Number,
urls: {
updatePaymentMethod: String,
customerPortal: String
},
cancelReason: String,
cancelledAt: Date,
metadata: mongoose.Schema.Types.Mixed
}, {
timestamps: true
});
module.exports = mongoose.model('Subscription', SubscriptionSchema);
const mongoose = require('mongoose');
const TradeSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
botId: {
type: String,
required: true
},
exchange: {
type: String,
required: true,
enum: ['binance', 'kucoin', 'coinbase', 'kraken']
},
pair: {
type: String,
required: true
},
direction: {
type: String,
enum: ['long', 'short'],
required: true
},
entryPrice: {
type: Number,
required: true
},
exitPrice: {
type: Number
},
amount: {
type: Number,
required: true
},
leverage: {
type: Number,
default: 1
},
status: {
type: String,
enum: ['pending', 'open', 'closed', 'cancelled', 'failed'],
default: 'pending'
},
pnl: {
type: Number
},
pnlPercentage: {
type: Number
},
fees: {
type: Number
},
strategy: {
type: String
},
indicators: mongoose.Schema.Types.Mixed,
notes: String,
closedAt: Date,
metadata: mongoose.Schema.Types.Mixed
}, {
timestamps: true
});
module.exports = mongoose.model('Trade', TradeSchema);
const mongoose = require('mongoose');
const AffiliateSchema = new mongoose.Schema({
affiliate: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
referredUser: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
commissionRate: {
type: Number,
default: 0.3 // 30% commission
},
commissionAmount: {
type: Number,
default: 0
},
status: {
type: String,
enum: ['pending', 'eligible', 'paid'],
default: 'pending'
},
paymentId: String,
paidAt: Date,
subscriptionTier: {
type: String,
enum: ['basic', 'pro', 'enterprise']
},
metadata: mongoose.Schema.Types.Mixed
}, {
timestamps: true
});
// Index for faster queries
AffiliateSchema.index({ affiliate: 1, referredUser: 1 }, { unique: true });
module.exports = mongoose.model('Affiliate', AffiliateSchema);
const mongoose = require('mongoose');
const QueueSchema = new mongoose.Schema({
jobId: {
type: String,
required: true,
unique: true
},
type: {
type: String,
required: true,
enum: ['trade', 'analysis', 'alert']
},
status: {
type: String,
enum: ['queued', 'processing', 'completed', 'failed', 'cancelled'],
default: 'queued'
},
priority: {
type: Number,
default: 0
},
data: mongoose.Schema.Types.Mixed,
result: mongoose.Schema.Types.Mixed,
error: mongoose.Schema.Types.Mixed,
startedAt: Date,
completedAt: Date,
attempts: {
type: Number,
default: 0
},
maxAttempts: {
type: Number,
default: 3
},
delay: {
type: Number,
default: 0
},
timeout: {
type: Number,
default: 30000 // 30 seconds
},
createdBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}
}, {
timestamps: true
});
module.exports = mongoose.model('Queue', QueueSchema);
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const mongoSanitize = require('express-mongo-sanitize');
const xss = require('xss-clean');
const hpp = require('hpp');
const cookieParser = require('cookie-parser');
const compression = require('compression');
const path = require('path');
const http = require('http');
const socketio = require('socket.io');
const logger = require('./utils/logger');
const errorHandler = require('./middleware/error');
const websocket = require('./services/websocket.service');
// Create express app
const app = express();
// Trust proxy
app.set('trust proxy', true);
// Enable CORS
app.use(cors({
origin: process.env.FRONTEND_URL,
credentials: true
}));
// Set security HTTP headers
app.use(helmet());
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 200, // limit each IP to 200 requests per windowMs
message: 'Too many requests from this IP, please try again later'
});
app.use('/api', limiter);
// Body parser, reading data from body into req.body
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true, limit: '10kb' }));
app.use(cookieParser());
// Data sanitization against NoSQL query injection
app.use(mongoSanitize());
// Data sanitization against XSS
app.use(xss());
// Prevent parameter pollution
app.use(hpp());
// Compress responses
app.use(compression());
// Static files
app.use(express.static(path.join(__dirname, 'public')));
// API routes
app.use('/api/v1/auth', require('./routes/auth.routes'));
app.use('/api/v1/users', require('./routes/user.routes'));
app.use('/api/v1/subscriptions', require('./routes/payment.routes'));
app.use('/api/v1/bot', require('./routes/bot.routes'));
app.use('/api/v1/affiliates', require('./routes/affiliate.routes'));
// Health check endpoint
app.get('/health', (req, res) => res.status(200).send('OK'));
// Handle 404
app.all('*', (req, res, next) => {
res.status(404).json({
status: 'fail',
message: `Can't find ${req.originalUrl} on this server!`
});
});
// Error handling middleware
app.use(errorHandler);
// Create HTTP server
const server = http.createServer(app);
// Set up Socket.io
const io = socketio(server, {
cors: {
origin: process.env.FRONTEND_URL,
methods: ['GET', 'POST'],
credentials: true
}
});
// Initialize WebSocket service
websocket.initialize(io);
module.exports = server;
const app = require('./app');
const mongoose = require('mongoose');
const config = require('./config/database');
const logger = require('./utils/logger');
const queueService = require('./services/queue.service');
// Handle uncaught exceptions
process.on('uncaughtException', err => {
logger.error('UNCAUGHT EXCEPTION! 💥 Shutting down...');
logger.error(err.name, err.message);
process.exit(1);
});
// Connect to database
mongoose.connect(config.uri, config.options)
.then(() => logger.info('DB connection successful!'))
.catch(err => {
logger.error('DB connection failed!');
logger.error(err);
process.exit(1);
});
// Start server
const port = process.env.PORT || 3000;
const server = app.listen(port, () => {
logger.info(`Server running on port ${port}...`);
});
// Initialize queues
queueService.initialize();
// Handle unhandled promise rejections
process.on('unhandledRejection', err => {
logger.error('UNHANDLED REJECTION! 💥 Shutting down...');
logger.error(err.name, err.message);
server.close(() => {
process.exit(1);
});
});
// Handle SIGTERM
process.on('SIGTERM', () => {
logger.info('👋 SIGTERM RECEIVED. Shutting down gracefully');
server.close(() => {
logger.info('💥 Process terminated!');
});
});
const logger = require('../utils/logger');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
class WebSocketService {
constructor() {
this.io = null;
this.connectedUsers = new Map();
}
initialize(io) {
this.io = io;
// Authentication middleware
io.use(async (socket, next) => {
try {
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error('Authentication error: Token not provided'));
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findById(decoded.id);
if (!user) {
return next(new Error('Authentication error: User not found'));
}
socket.user = user;
next();
} catch (err) {
next(new Error('Authentication error: Invalid token'));
}
});
// Connection handler
io.on('connection', (socket) => {
logger.info(`New WebSocket connection: ${socket.id}`);
// Store user connection
if (socket.user) {
this.connectedUsers.set(socket.user._id.toString(), socket);
logger.info(`User ${socket.user.email} connected via WebSocket`);
}
// Trade updates subscription
socket.on('subscribe:trades', (data) => {
if (socket.user) {
socket.join(`user:${socket.user._id}:trades`);
logger.info(`User ${socket.user.email} subscribed to trade updates`);
}
});
// Bot status subscription
socket.on('subscribe:bot-status', (data) => {
if (socket.user) {
socket.join(`user:${socket.user._id}:bot-status`);
logger.info(`User ${socket.user.email} subscribed to bot status updates`);
}
});
// Queue updates subscription
socket.on('subscribe:queue', (data) => {
if (socket.user) {
socket.join(`user:${socket.user._id}:queue`);
logger.info(`User ${socket.user.email} subscribed to queue updates`);
}
});
// Disconnection handler
socket.on('disconnect', () => {
logger.info(`WebSocket disconnected: ${socket.id}`);
if (socket.user) {
this.connectedUsers.delete(socket.user._id.toString());
}
});
});
}
// Send trade update to specific user
sendTradeUpdate(userId, trade) {
if (this.io) {
this.io.to(`user:${userId}:trades`).emit('trade:update', trade);
}
}
// Send bot status update to specific user
sendBotStatusUpdate(userId, status) {
if (this.io) {
this.io.to(`user:${userId}:bot-status`).emit('bot:status', status);
}
}
// Send queue update to specific user
sendQueueUpdate(userId, queueItem) {
if (this.io) {
this.io.to(`user:${userId}:queue`).emit('queue:update', queueItem);
}
}
// Send notification to specific user
sendNotification(userId, notification) {
if (this.io) {
const socket = this.connectedUsers.get(userId.toString());
if (socket) {
socket.emit('notification', notification);
}
}
}
}
module.exports = new WebSocketService();
const Queue = require('bull');
const mongoose = require('mongoose');
const logger = require('../utils/logger');
const Trade = require('../models/Trade');
const QueueModel = require('../models/Queue');
const websocket = require('./websocket.service');
const { executeTrade } = require('./bot.service');
class QueueService {
constructor() {
this.tradingQueue = null;
}
initialize() {
// Create trading queue
this.tradingQueue = new Queue('trading', {
redis: {
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
password: process.env.REDIS_PASSWORD
},
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: true,
attempts: 3,
backoff: {
type: 'exponential',
delay: 5000
}
}
});
// Process queue jobs
this.tradingQueue.process('execute-trade', async (job) => {
const { userId, tradeData } = job.data;
try {
// Update queue status in DB
await QueueModel.findOneAndUpdate(
{ jobId: job.id.toString() },
{ status: 'processing', startedAt: new Date() },
{ new: true }
);
// Execute the trade
const trade = await executeTrade(userId, tradeData);
// Update queue status
await QueueModel.findOneAndUpdate(
{ jobId: job.id.toString() },
{ status: 'completed', completedAt: new Date(), result: trade },
{ new: true }
);
// Send WebSocket update
websocket.sendQueueUpdate(userId, {
jobId: job.id.toString(),
status: 'completed',
trade
});
return trade;
} catch (err) {
// Update queue status
await QueueModel.findOneAndUpdate(
{ jobId: job.id.toString() },
{
status: 'failed',
completedAt: new Date(),
error: err.message,
attempts: job.attemptsMade
},
{ new: true }
);
// Send WebSocket update
websocket.sendQueueUpdate(userId, {
jobId: job.id.toString(),
status: 'failed',
error: err.message
});
throw err;
}
});
// Event listeners
this.tradingQueue.on('completed', (job, result) => {
logger.info(`Job ${job.id} completed with result:`, result);
});
this.tradingQueue.on('failed', (job, err) => {
logger.error(`Job ${job.id} failed with error:`, err);
});
this.tradingQueue.on('error', (err) => {
logger.error('Queue error:', err);
});
}
async addTradeToQueue(userId, tradeData) {
try {
// Add job to queue
const job = await this.tradingQueue.add('execute-trade', { userId, tradeData }, {
priority: tradeData.priority || 0,
delay: tradeData.delay || 0
});
// Save to database
const queueItem = new QueueModel({
jobId: job.id.toString(),
type: 'trade',
status: 'queued',
priority: tradeData.priority || 0,
data: tradeData,
createdBy: userId
});
await queueItem.save();
// Send WebSocket update
websocket.sendQueueUpdate(userId, {
jobId: job.id.toString(),
status: 'queued',
position: await job.getState()
});
return queueItem;
} catch (err) {
logger.error('Error adding to queue:', err);
throw err;
}
}
async getQueueStatus(userId) {
try {
const jobs = await this.tradingQueue.getJobs(['waiting', 'active', 'completed', 'failed']);
// Filter jobs for this user
const userJobs = jobs.filter(job => job.data.userId.toString() === userId.toString());
// Get from database for more details
const queueItems = await QueueModel.find({
createdBy: userId,
status: { $in: ['queued', 'processing'] }
}).sort('-createdAt');
return {
waiting: await this.tradingQueue.getWaitingCount(),
active: await this.tradingQueue.getActiveCount(),
completed: await this.tradingQueue.getCompletedCount(),
failed: await this.tradingQueue.getFailedCount(),
userJobs: queueItems
};
} catch (err) {
logger.error('Error getting queue status:', err);
throw err;
}
}
}
module.exports = new QueueService();
const axios = require('axios');
const crypto = require('crypto');
const logger = require('../utils/logger');
const Subscription = require('../models/Subscription');
const User = require('../models/User');
const Affiliate = require('../models/Affiliate');
const websocket = require('./websocket.service');
class PaymentService {
constructor() {
this.apiUrl = 'https://api.lemonsqueezy.com/v1';
this.headers = {
'Accept': 'application/vnd.api+json',
'Content-Type': 'application/vnd.api+json',
'Authorization': `Bearer ${process.env.LEMON_SQUEEZY_API_KEY}`
};
}
async createCheckout(user, variantId, affiliateCode = null) {
try {
// Create checkout URL
const response = await axios.post(`${this.apiUrl}/checkouts`, {
data: {
type: 'checkouts',
attributes: {
checkout_data: {
email: user.email,
custom: {
user_id: user._id.toString(),
affiliate_code: affiliateCode
}
},
product_options: {
enabled_variants: [variantId],
redirect_url: `${process.env.FRONTEND_URL}/dashboard`,
receipt_button_text: 'Go to Dashboard',
receipt_thank_you_note: 'Thank you for subscribing to Sentimint!'
},
expires_at: null,
test_mode: process.env.NODE_ENV !== 'production'
},
relationships: {
store: {
data: {
type: 'stores',
id: process.env.LEMON_SQUEEZY_STORE_ID
}
},
variant: {
data: {
type: 'variants',
id: variantId
}
}
}
}
}, { headers: this.headers });
return response.data.data.attributes.url;
} catch (err) {
logger.error('Error creating checkout:', err.response?.data || err.message);
throw err;
}
}
async handleWebhook(payload, signature) {
try {
// Verify webhook signature
const hmac = crypto.createHmac('sha256', process.env.LEMON_SQUEEZY_WEBHOOK_SECRET);
const digest = hmac.update(JSON.stringify(payload)).digest('hex');
if (signature !== digest) {
throw new Error('Invalid webhook signature');
}
const eventName = payload.meta.event_name;
const eventData = payload.data;
logger.info(`Processing LemonSqueezy webhook: ${eventName}`);
// Handle different event types
switch (eventName) {
case 'order_created':
await this.handleOrderCreated(eventData);
break;
case 'subscription_created':
await this.handleSubscriptionCreated(eventData);
break;
case 'subscription_updated':
await this.handleSubscriptionUpdated(eventData);
break;
case 'subscription_cancelled':
await this.handleSubscriptionCancelled(eventData);
break;
case 'subscription_expired':
await this.handleSubscriptionExpired(eventData);
break;
case 'subscription_resumed':
await this.handleSubscriptionResumed(eventData);
break;
case 'subscription_payment_success':
await this.handlePaymentSuccess(eventData);
break;
case 'subscription_payment_failed':
await this.handlePaymentFailed(eventData);
break;
case 'subscription_payment_recovered':
await this.handlePaymentRecovered(eventData);
break;
default:
logger.info(`Unhandled webhook event: ${eventName}`);
}
return { success: true };
} catch (err) {
logger.error('Error processing webhook:', err);
throw err;
}
}
async handleOrderCreated(data) {
const customData = data.attributes.custom_data || {};
const userId = customData.user_id;
const affiliateCode = customData.affiliate_code;
if (!userId) return;
// Check if this is a subscription purchase
const includedSubscriptions = payload.included?.filter(item => item.type === 'subscriptions');
if (!includedSubscriptions || includedSubscriptions.length === 0) return;
// Handle affiliate commission if applicable
if (affiliateCode) {
const affiliateUser = await User.findOne({ affiliateCode });
if (affiliateUser) {
const tier = this.getTierFromVariantId(data.attributes.variant_id);
const affiliateRecord = new Affiliate({
affiliate: affiliateUser._id,
referredUser: userId,
subscriptionTier: tier,
status: 'pending'
});
await affiliateRecord.save();
// Notify affiliate
websocket.sendNotification(affiliateUser._id, {
type: 'affiliate',
message: `New referral: ${data.attributes.user_email}`
});
}
}
}
async handleSubscriptionCreated(data) {
const userId = data.attributes.user_id;
if (!userId) return;
const user = await User.findById(userId);
if (!user) return;
const variantId = data.attributes.variant_id;
const tier = this.getTierFromVariantId(variantId);
// Create or update subscription
const subscription = await Subscription.findOneAndUpdate(
{ user: userId },
{
lemonSqueezyId: data.id,
orderId: data.attributes.order_id,
productId: tier,
status: 'active',
currentPeriodEnd: new Date(data.attributes.renews_at),
renewsAt: new Date(data.attributes.renews_at),
urls: {
updatePaymentMethod: data.attributes.urls.update_payment_method,
customerPortal: data.attributes.urls.customer_portal
},
paymentMethod: data.attributes.payment_method,
billingAnchor: data.attributes.billing_anchor
},
{ upsert: true, new: true }
);
// Enable trading for user
user.tradingEnabled = true;
await user.save();
// Send notification
websocket.sendNotification(userId, {
type: 'subscription',
message: `Your ${tier} subscription is now active!`
});
return subscription;
}
async handleSubscriptionUpdated(data) {
const subscription = await Subscription.findOne({ lemonSqueezyId: data.id });
if (!subscription) return;
// Update subscription details
subscription.currentPeriodEnd = new Date(data.attributes.renews_at);
subscription.renewsAt = new Date(data.attributes.renews_at);
subscription.paymentMethod = data.attributes.payment_method;
subscription.urls.updatePaymentMethod = data.attributes.urls.update_payment_method;
subscription.urls.customerPortal = data.attributes.urls.customer_portal;
await subscription.save();
// Send notification
websocket.sendNotification(subscription.user, {
type: 'subscription',
message: 'Your subscription has been updated'
});
}
async handleSubscriptionCancelled(data) {
const subscription = await Subscription.findOne({ lemonSqueezyId: data.id });
if (!subscription) return;
// Update subscription status
subscription.status = 'cancelled';
subscription.cancelReason = data.attributes.cancellation_reason;
subscription.cancelledAt = new Date(data.attributes.ends_at);
await subscription.save();
// Disable trading for user
const user = await User.findById(subscription.user);
if (user) {
user.tradingEnabled = false;
await user.save();
}
// Send notification
websocket.sendNotification(subscription.user, {
type: 'subscription',
message: 'Your subscription has been cancelled'
});
}
async handlePaymentSuccess(data) {
const subscription = await Subscription.findOne({ lemonSqueezyId: data.attributes.subscription_id });
if (!subscription) return;
// Update subscription renewal date
subscription.currentPeriodEnd = new Date(data.attributes.renews_at);
subscription.renewsAt = new Date(data.attributes.renews_at);
await subscription.save();
// Handle affiliate commission
const affiliateRecord = await Affiliate.findOne({
referredUser: subscription.user,
status: 'pending'
});
if (affiliateRecord) {
const tier = subscription.productId;
let commissionAmount = 0;
// Calculate commission based on tier
if (tier === 'basic') commissionAmount = 99 * 0.3; // 30% of $99
if (tier === 'pro') commissionAmount = 299 * 0.3; // 30% of $299
if (tier === 'enterprise') commissionAmount = 999 * 0.3; // 30% of $999
affiliateRecord.commissionAmount = commissionAmount;
affiliateRecord.status = 'eligible';
await affiliateRecord.save();
// Notify affiliate
websocket.sendNotification(affiliateRecord.affiliate, {
type: 'affiliate',
message: `You've earned $${commissionAmount.toFixed(2)} from a referral!`
});
}
// Send notification to user
websocket.sendNotification(subscription.user, {
type: 'payment',
message: 'Your subscription payment was successful'
});
}
getTierFromVariantId(variantId) {
// Map LemonSqueezy variant IDs to subscription tiers
const variantMap = {
[process.env.LEMON_SQUEEZY_BASIC_VARIANT_ID]: 'basic',
[process.env.LEMON_SQUEEZY_PRO_VARIANT_ID]: 'pro',
[process.env.LEMON_SQUEEZY_ENTERPRISE_VARIANT_ID]: 'enterprise'
};
return variantMap[variantId] || 'basic';
}
}
module.exports = new PaymentService();
# Server
NODE_ENV=development
PORT=3000
FRONTEND_URL=http://localhost:3000
# Database
MONGODB_URI=mongodb://localhost:27017/sentimint
MONGODB_OPTIONS={}
# JWT
JWT_SECRET=your_jwt_secret
JWT_EXPIRE=30d
JWT_COOKIE_EXPIRE=30
# LemonSqueezy
LEMON_SQUEEZY_API_KEY=your_api_key
LEMON_SQUEEZY_STORE_ID=your_store_id
LEMON_SQUEEZY_WEBHOOK_SECRET=your_webhook_secret
LEMON_SQUEEZY_BASIC_VARIANT_ID=your_basic_variant_id
LEMON_SQUEEZY_PRO_VARIANT_ID=your_pro_variant_id
LEMON_SQUEEZY_ENTERPRISE_VARIANT_ID=your_enterprise_variant_id
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
# Email (optional)
SMTP_HOST=
SMTP_PORT=
SMTP_USERNAME=
SMTP_PASSWORD=
EMAIL_FROM=
# Clone the repository git clone https://github.com/your-repo/sentimint-backend.git cd sentimint-backend # Install dependencies npm install # Create .env file and configure environment variables cp .env.example .env nano .env # Start the server npm run dev
# Build for production npm run build # Start in production mode npm start # Using PM2 (recommended) npm install -g pm2 pm2 start dist/server.js --name sentimint-backend
// src/api/client.js
import axios from 'axios';
const apiClient = axios.create({
baseURL: process.env.REACT_APP_API_URL || 'http://localhost:3000/api/v1',
withCredentials: true
});
// Add request interceptor for JWT
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Add response interceptor for error handling
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Handle unauthorized (token expired)
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default apiClient;
// src/api/websocket.js
import io from 'socket.io-client';
let socket;
export const connectWebSocket = (token) => {
socket = io(process.env.REACT_APP_API_URL || 'http://localhost:3000', {
auth: { token },
transports: ['websocket']
});
return socket;
};
export const disconnectWebSocket = () => {
if (socket) {
socket.disconnect();
}
};
export const getSocket = () => socket;
// Example API calls from React frontend
import apiClient from './client';
// User registration
export const register = async (userData) => {
const response = await apiClient.post('/auth/register', userData);
return response.data;
};
// User login
export const login = async (credentials) => {
const response = await apiClient.post('/auth/login', credentials);
return response.data;
};
// Get user profile
export const getMe = async () => {
const response = await apiClient.get('/users/me');
return response.data;
};
// Start trading bot
export const startBot = async (strategy) => {
const response = await apiClient.post('/bot/start', { strategy });
return response.data;
};
// Get trade history
export const getTrades = async (params) => {
const response = await apiClient.get('/bot/trades', { params });
return response.data;
};