Skip to content
67 changes: 51 additions & 16 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,20 +1,43 @@
CLOUDINARY_API_KEY=your_cloudinary_api_key
CLOUDINARY_API_SECRET=your_cloudinary_api_secret
CLOUDINARY_CLOUD_NAME=your_cloudinary_cloud_name
CLOUDINARY_URL=your_cloudinary_url
JWT_SECRET=your_jwt_secret
MONGO_URI="your_mongodb_uri"
PORT=5000
# DeenBridge Backend Environment Variables

# MongoDB connection string
MONGO_URI=mongodb+srv://user:password@cluster.mongodb.net/dnb-backend?retryWrites=true&w=majority

# JWT secret for authentication (use a strong random string, min 32 characters)
JWT_SECRET=your_jwt_secret_here

# Node environment (development, production, test)
NODE_ENV=development

# Server port
PORT=5000

# Cloudinary configuration for file uploads
CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret
CLOUDINARY_URL=cloudinary://api_key:api_secret@cloud_name

# EmailJS configuration for sending emails
EMAILJS_API_URL=https://api.emailjs.com/api/v1.0/email/send
EMAILJS_PRIVATE_KEY=your_emailjs_private_key
EMAILJS_PUBLIC_KEY=your_emailjs_public_key
EMAILJS_SERVICE_ID=your_emailjs_service_id
EMAILJS_TEMPLATE_ID=your_emailjs_template_id
EMAILJS_RECEIPT_TEMPLATE_ID=your_emailjs_receipt_template_id
EMAILJS_PRIVATE_KEY=your_private_key
EMAILJS_PUBLIC_KEY=your_public_key
EMAILJS_SERVICE_ID=your_service_id
EMAILJS_TEMPLATE_ID=your_template_id

# Stellar blockchain network (testnet or mainnet)
STELLAR_NETWORK=testnet

# Resilient Horizon Client Configuration (Optional)
# HORIZON_URLS=https://horizon-testnet.stellar.org,https://horizon-testnet.stellar.org (Comma-separated list of Horizon endpoints)
# HORIZON_TIMEOUT_MS=10000 (Request timeout in milliseconds)
# HORIZON_MAX_RETRIES=3 (Maximum number of retries for transient errors)
# HORIZON_CB_THRESHOLD=5 (Number of consecutive failures before opening the circuit breaker)
# HORIZON_CB_COOLDOWN_MS=30000 (Time to wait in ms before attempting a half-open probe)

# Stellar donation fund (public key only - the secret key must NEVER be stored here)
DONATION_WALLET_PUBLIC_KEY=GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

# Platform fee split on purchases (0-20, 0 disables the split)
PLATFORM_FEE_PERCENT=0
PLATFORM_WALLET_PUBLIC_KEY=GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Expand All @@ -23,8 +46,20 @@ PLATFORM_WALLET_PUBLIC_KEY=GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
ACCESS_TOKEN_TTL=15m
REFRESH_TOKEN_TTL=30d

# Durable Mongo-backed background jobs (tests use inline)
JOBS_ENABLED=true
QUEUE_DRIVER=mongo
JOBS_DASHBOARD_TOKEN=replace_with_a_long_random_token
# Redis Configuration (optional - app works without Redis but with reduced performance)
# Option 1: Use REDIS_URL for full connection string (recommended for cloud services)
# REDIS_URL=redis://username:password@host:port

# Option 2: Use separate credentials
REDIS_HOST=localhost
REDIS_PORT=6379
# REDIS_USERNAME=default
# REDIS_PASSWORD=your_password

# Jitsi configuration for video calls (optional)
# JITSI_MEET_DOMAIN=your_jitsi_domain
# JITSI_APP_ID=your_app_id
# JITSI_PRIVATE_KEY=your_private_key
# JITSI_PUBLIC_KEY_ID=your_public_key_id
# JITSI_KID=your_kid
# JITSI_TENANT=your_tenant
9 changes: 9 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import callRoutes from "./src/routes/callRoutes.js";
import stellarWalletRoutes from "./src/routes/stellar/walletRoutes.js";
import stellarPaymentRoutes from "./src/routes/stellar/paymentRoutes.js";
import stellarDonationRoutes from "./src/routes/stellar/donationRoutes.js";
import stellarGiftRoutes from "./src/routes/stellar/giftRoutes.js";
import payoutRoutes from "./src/routes/payoutRoutes.js";
import jobsRoutes from "./src/routes/jobsRoutes.js";

Expand Down Expand Up @@ -180,6 +181,7 @@ app.use("/api/calls", callRoutes);
app.use("/api/stellar/wallet", stellarWalletRoutes);
app.use("/api/stellar/payment", stellarPaymentRoutes);
app.use("/api/stellar/donation", stellarDonationRoutes);
app.use("/api/stellar/gifts", stellarGiftRoutes);
app.use("/api/payouts", payoutRoutes);
app.use("/admin/jobs", jobsRoutes);

Expand All @@ -191,6 +193,13 @@ app.use(notFound);
app.use(errorHandler);
handleUnhandledRejection();

import { startGiftSweepJob } from "./src/jobs/sweepExpiredGifts.js";

// Start background jobs
if (process.env.NODE_ENV !== "test") {
startGiftSweepJob();
}

logger.info("DeenBridge API initialized");
logger.info(`Logging enabled - Level: ${logger.level}`);

Expand Down
28 changes: 24 additions & 4 deletions src/config/validateEnv.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,37 @@ const optionalEnvVars = [
"PAYOUT_ADMIN_USER_IDS",
"ACCESS_TOKEN_TTL",
"REFRESH_TOKEN_TTL",
"QUEUE_DRIVER",
"JOBS_ENABLED",
"JOBS_DASHBOARD_TOKEN",
"EMAILJS_RECEIPT_TEMPLATE_ID",
// Redis configuration (optional - app works without Redis)
"REDIS_URL",
"REDIS_HOST",
"REDIS_PORT",
"REDIS_USERNAME",
"REDIS_PASSWORD",
"HORIZON_URLS",
"HORIZON_TIMEOUT_MS",
"HORIZON_MAX_RETRIES",
"HORIZON_CB_THRESHOLD",
"HORIZON_CB_COOLDOWN_MS",
];

export const validateEnv = () => {
// Default values for TTLs if not provided
process.env.ACCESS_TOKEN_TTL = process.env.ACCESS_TOKEN_TTL || "15m";
process.env.REFRESH_TOKEN_TTL = process.env.REFRESH_TOKEN_TTL || "30d";

// Default values for Horizon resilient client if not provided
const network = process.env.STELLAR_NETWORK || "testnet";
if (!process.env.HORIZON_URLS) {
process.env.HORIZON_URLS =
network === "mainnet"
? "https://horizon.stellar.org"
: "https://horizon-testnet.stellar.org";
}
process.env.HORIZON_TIMEOUT_MS = process.env.HORIZON_TIMEOUT_MS || "10000";
process.env.HORIZON_MAX_RETRIES = process.env.HORIZON_MAX_RETRIES || "3";
process.env.HORIZON_CB_THRESHOLD = process.env.HORIZON_CB_THRESHOLD || "5";
process.env.HORIZON_CB_COOLDOWN_MS = process.env.HORIZON_CB_COOLDOWN_MS || "30000";

const missing = [];

requiredEnvVars.forEach((envVar) => {
Expand Down
57 changes: 57 additions & 0 deletions src/config/validateEnv.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { jest } from "@jest/globals";
import { validateEnv } from "./validateEnv.js";

describe("validateEnv", () => {
const originalEnv = process.env;

beforeEach(() => {
jest.resetModules();
process.env = {
...originalEnv,
MONGO_URI: "mongodb://localhost:27017/test",
JWT_SECRET: "test-secret-key-for-ci-minimum-32-chars",
NODE_ENV: "test",
PORT: "5000",
};
// Ensure new vars are unset
delete process.env.HORIZON_URLS;
delete process.env.HORIZON_TIMEOUT_MS;
delete process.env.HORIZON_MAX_RETRIES;
delete process.env.HORIZON_CB_THRESHOLD;
delete process.env.HORIZON_CB_COOLDOWN_MS;
});

afterAll(() => {
process.env = originalEnv;
});

it("should derive testnet default endpoint when STELLAR_NETWORK is unset or testnet", () => {
delete process.env.STELLAR_NETWORK;
validateEnv();
expect(process.env.HORIZON_URLS).toBe("https://horizon-testnet.stellar.org");
expect(process.env.HORIZON_TIMEOUT_MS).toBe("10000");
expect(process.env.HORIZON_MAX_RETRIES).toBe("3");
expect(process.env.HORIZON_CB_THRESHOLD).toBe("5");
expect(process.env.HORIZON_CB_COOLDOWN_MS).toBe("30000");
});

it("should derive mainnet default endpoint when STELLAR_NETWORK is mainnet", () => {
process.env.STELLAR_NETWORK = "mainnet";
validateEnv();
expect(process.env.HORIZON_URLS).toBe("https://horizon.stellar.org");
});

it("should preserve explicitly set Horizon values", () => {
process.env.HORIZON_URLS = "https://custom.stellar.org";
process.env.HORIZON_TIMEOUT_MS = "5000";
process.env.HORIZON_MAX_RETRIES = "1";
process.env.HORIZON_CB_THRESHOLD = "10";
process.env.HORIZON_CB_COOLDOWN_MS = "10000";
validateEnv();
expect(process.env.HORIZON_URLS).toBe("https://custom.stellar.org");
expect(process.env.HORIZON_TIMEOUT_MS).toBe("5000");
expect(process.env.HORIZON_MAX_RETRIES).toBe("1");
expect(process.env.HORIZON_CB_THRESHOLD).toBe("10");
expect(process.env.HORIZON_CB_COOLDOWN_MS).toBe("10000");
});
});
Loading
Loading