Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 9 additions & 22 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,11 @@ 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_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)
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
STELLAR_NETWORK=testnet

# Resilient Horizon Client Configuration (Optional)
Expand All @@ -46,20 +45,8 @@ PLATFORM_WALLET_PUBLIC_KEY=GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
ACCESS_TOKEN_TTL=15m
REFRESH_TOKEN_TTL=30d

# 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
# Durable Mongo-backed background jobs (tests use inline)
JOBS_ENABLED=true
QUEUE_DRIVER=mongo
JOBS_DASHBOARD_TOKEN=replace_with_a_long_random_token

# 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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,22 @@ The API runs at `http://localhost:5000`.
| `JWT_SECRET` | Secret for signing tokens (32+ chars) |
| `STELLAR_NETWORK` | `testnet` or `mainnet` |
| `CLOUDINARY_*` | Cloudinary credentials for media uploads |
| `QUEUE_DRIVER` | `mongo` (durable production default) or `inline` (tests/CI) |
| `JOBS_ENABLED` | Start background workers; defaults to `true` |
| `JOBS_DASHBOARD_TOKEN` | Bearer token protecting `/admin/jobs` |

See `.env.example` for the full list.

### Background jobs

Slow email and Stellar verification work uses the thin `src/jobs/queue.js`
abstraction. The production `mongo` driver persists jobs in the mandatory
MongoDB deployment, so work and idempotency keys survive restarts without
making optional Redis a new requirement. The `inline` driver uses the same
handlers in tests and CI. Jobs retry with exponential backoff and jitter,
terminal failures are queryable at `/admin/jobs/dead`, and the token-protected
dashboard is available at `/admin/jobs`.

### Scripts

| Command | Purpose |
Expand All @@ -89,6 +102,7 @@ See `.env.example` for the full list.
| `npm start` | Start in production mode |
| `npm test` | Run the Jest + Supertest suite |
| `npm run seed` | Seed sample data |
| `npm run seed:categories` | Idempotent migration/seed script to populate initial categories based on current course categories |

## 🔗 API Overview

Expand Down
5 changes: 5 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import cookieParser from "cookie-parser";
import compression from "compression";
import dotenv from "dotenv";
import crypto from "crypto";
import "./src/jobs/handlers.js";

dotenv.config();

Expand Down Expand Up @@ -43,6 +44,8 @@ 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 payoutRoutes from "./src/routes/payoutRoutes.js";
import jobsRoutes from "./src/routes/jobsRoutes.js";
import categoryRoutes from "./src/routes/categoryRoutes.js";

handleUncaughtException();
validateEnv();
Expand Down Expand Up @@ -165,6 +168,7 @@ app.use("/api", apiLimiter);
app.use("/api/auth", authLimiter, authRoutes);

// Other API routes
app.use("/api/categories", categoryRoutes);
app.use("/api/courses", courseRoutes);
app.use("/api/reels", reelsRoute);
app.use("/api/books", bookRoutes);
Expand All @@ -179,6 +183,7 @@ app.use("/api/stellar/wallet", stellarWalletRoutes);
app.use("/api/stellar/payment", stellarPaymentRoutes);
app.use("/api/stellar/donation", stellarDonationRoutes);
app.use("/api/payouts", payoutRoutes);
app.use("/admin/jobs", jobsRoutes);

// ======================
// ERROR HANDLING
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --forceExit",
"start": "node server.js",
"dev": "nodemon server.js",
"seed": "node src/scripts/seedDatabase.js",
"seed:categories": "node src/scripts/seedCategories.js",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve or update the documented seed command.

Replacing seed breaks the npm run seed command still documented in README.md. Restore the old alias or update the documentation and migration instructions together.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 9, The package scripts no longer provide the documented
npm run seed command. Update the package.json script configuration around
seed:categories to restore the seed alias, or consistently rename the command in
README.md and migration instructions while preserving the documented seeding
workflow.

"test-redis": "node test-redis.js",
"payouts:audit": "node src/scripts/auditPayouts.js"
},
Expand Down
6 changes: 6 additions & 0 deletions server.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import app from "./app.js";
import logger from "./src/config/logger.js";
import { initRedis, closeRedis } from "./src/config/redis.js";
import { startJobs, stopJobs } from "./src/jobs/queue.js";
import "./src/jobs/handlers.js";

const PORT = process.env.PORT || 5000;

Expand All @@ -18,13 +20,17 @@ const server = app.listen(PORT, () => {
logger.info(`Process ID: ${process.pid}`);
});

startJobs().catch((err) => logger.error(err, "Background job startup failed"));

// Graceful shutdown
const gracefulShutdown = async (signal) => {
logger.info(`${signal} received. Starting graceful shutdown...`);

server.close(async () => {
logger.info("HTTP server closed");

await stopJobs();

// Close Redis connection
await closeRedis();

Expand Down
41 changes: 28 additions & 13 deletions services/emails/sendMail.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
import axios from "axios";
import logger from "../../src/config/logger.js";

const EMAILJS_SERVICE_ID = process.env.EMAILJS_SERVICE_ID || "service_5cyu19r";
const EMAILJS_TEMPLATE_ID = process.env.EMAILJS_TEMPLATE_ID || "template_ph4f0rl";
const EMAILJS_PRIVATE_KEY = process.env.EMAILJS_PRIVATE_KEY || "xsR_t0uapauk8d_6Tk0Y9";
const EMAILJS_PUBLIC_KEY = process.env.EMAILJS_PUBLIC_KEY || "p_hFoYPb6o0w7206-";
const EMAILJS_SERVICE_ID = process.env.EMAILJS_SERVICE_ID;
const EMAILJS_TEMPLATE_ID = process.env.EMAILJS_TEMPLATE_ID;
const EMAILJS_RECEIPT_TEMPLATE_ID = process.env.EMAILJS_RECEIPT_TEMPLATE_ID || EMAILJS_TEMPLATE_ID;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Silent fallback to the OTP template for receipts risks sending the wrong email content.

If EMAILJS_RECEIPT_TEMPLATE_ID is unset, EMAILJS_RECEIPT_TEMPLATE_ID becomes EMAILJS_TEMPLATE_ID (the OTP template). sendTemplate's guard only checks that a templateId is truthy, so this passes validation and silently sends receipt data through the OTP template — likely rendering garbage or the wrong message to the donor. Prefer failing loudly (throw if EMAILJS_RECEIPT_TEMPLATE_ID is missing) over reusing an unrelated template. As per path instructions, this configuration belongs in .env.example.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/emails/sendMail.js` at line 6, Remove the fallback from
EMAILJS_RECEIPT_TEMPLATE_ID so it cannot reuse EMAILJS_TEMPLATE_ID; validate
that the receipt template ID is configured and fail loudly when it is missing.
Also add the required EMAILJS_RECEIPT_TEMPLATE_ID configuration entry to
.env.example.

Source: Path instructions

const EMAILJS_PRIVATE_KEY = process.env.EMAILJS_PRIVATE_KEY;
const EMAILJS_PUBLIC_KEY = process.env.EMAILJS_PUBLIC_KEY;
const EMAILJS_API_URL = process.env.EMAILJS_API_URL || "https://api.emailjs.com/api/v1.0/email/send";

const sendMail = async (otp, email) => {
if (!email || !otp) {
throw new Error("Email and OTP are required to send the email.");
const sendTemplate = async (templateId, email, templateParams) => {
if (
process.env.NODE_ENV !== "test" &&
(!EMAILJS_SERVICE_ID || !templateId || !EMAILJS_PUBLIC_KEY)
) {
throw new Error("EmailJS environment variables are not configured");
}
logger.info(`Sending OTP email to: ${email}`);
try {
const response = await axios.post(EMAILJS_API_URL, {
service_id: EMAILJS_SERVICE_ID,
template_id: EMAILJS_TEMPLATE_ID,
template_id: templateId,
user_id: EMAILJS_PUBLIC_KEY,
template_params: {
"otp": otp,
"email": email,
},
accessToken: EMAILJS_PRIVATE_KEY,
template_params: { ...templateParams, email },
});
return { status: response.status, text: response.statusText };
} catch (error) {
Expand All @@ -29,4 +30,18 @@ const sendMail = async (otp, email) => {
}
};

export const sendOtpEmail = async (otp, email) => {
if (!email || !otp) throw new Error("Email and OTP are required to send the email.");
logger.info(`Sending OTP email to: ${email}`);
return sendTemplate(EMAILJS_TEMPLATE_ID, email, { otp });
};

export const sendReceiptEmail = async (receipt) => {
if (!receipt.email || !receipt.txHash) throw new Error("Receipt email and transaction hash are required");
logger.info(`Sending receipt for ${receipt.txHash} to: ${receipt.email}`);
return sendTemplate(EMAILJS_RECEIPT_TEMPLATE_ID, receipt.email, receipt);
};
Comment on lines +33 to +43

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Avoid logging raw email addresses.

Sending OTP email to: ${email} and Sending receipt for ${receipt.txHash} to: ${receipt.email} write plaintext user emails to logs. Static analysis flags this too. Prefer logging a non-identifying reference (e.g., userId, or a masked/hashed email) to avoid PII retention in log stores.

🛡 Proposed fix
-  logger.info(`Sending OTP email to: ${email}`);
+  logger.info("Sending OTP email");
...
-  logger.info(`Sending receipt for ${receipt.txHash} to: ${receipt.email}`);
+  logger.info(`Sending receipt for ${receipt.txHash}`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const sendOtpEmail = async (otp, email) => {
if (!email || !otp) throw new Error("Email and OTP are required to send the email.");
logger.info(`Sending OTP email to: ${email}`);
return sendTemplate(EMAILJS_TEMPLATE_ID, email, { otp });
};
export const sendReceiptEmail = async (receipt) => {
if (!receipt.email || !receipt.txHash) throw new Error("Receipt email and transaction hash are required");
logger.info(`Sending receipt for ${receipt.txHash} to: ${receipt.email}`);
return sendTemplate(EMAILJS_RECEIPT_TEMPLATE_ID, receipt.email, receipt);
};
export const sendOtpEmail = async (otp, email) => {
if (!email || !otp) throw new Error("Email and OTP are required to send the email.");
logger.info("Sending OTP email");
return sendTemplate(EMAILJS_TEMPLATE_ID, email, { otp });
};
export const sendReceiptEmail = async (receipt) => {
if (!receipt.email || !receipt.txHash) throw new Error("Receipt email and transaction hash are required");
logger.info(`Sending receipt for ${receipt.txHash}`);
return sendTemplate(EMAILJS_RECEIPT_TEMPLATE_ID, receipt.email, receipt);
};
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 34-34: Avoid logging sensitive data
Context: logger.info(Sending OTP email to: ${email})
Note: [CWE-532] Insertion of Sensitive Information into Log File.

(log-sensitive-data)


[warning] 40-40: Avoid logging sensitive data
Context: logger.info(Sending receipt for ${receipt.txHash} to: ${receipt.email})
Note: [CWE-532] Insertion of Sensitive Information into Log File.

(log-sensitive-data)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/emails/sendMail.js` around lines 33 - 43, Update sendOtpEmail and
sendReceiptEmail to stop logging plaintext email addresses; log a
non-identifying reference such as an available userId or masked/hashed email
instead, while preserving the existing email delivery behavior and
transaction-hash context.

Source: Linters/SAST tools


const sendMail = sendOtpEmail;

export default sendMail;
15 changes: 4 additions & 11 deletions src/config/validateEnv.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,10 @@ const optionalEnvVars = [
"PAYOUT_ADMIN_USER_IDS",
"ACCESS_TOKEN_TTL",
"REFRESH_TOKEN_TTL",
// 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",
"QUEUE_DRIVER",
"JOBS_ENABLED",
"JOBS_DASHBOARD_TOKEN",
"EMAILJS_RECEIPT_TEMPLATE_ID",
];

export const validateEnv = () => {
Expand Down
15 changes: 7 additions & 8 deletions src/controllers/authController.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Session from "../models/Session.js";
import sendMail from "../../services/emails/sendMail.js";
import { generatedOtp } from "../routes/emailRoutes.js";
import logger from "../config/logger.js";
import { enqueue } from "../jobs/queue.js";

import { catchAsync, APIError } from "../middlewares/errorHandler.js";

Expand Down Expand Up @@ -111,14 +112,6 @@ export const registerUser = catchAsync(async (req, res, next) => {
return next(new APIError("Email already exists", 400));
}

// Send OTP email
try {
await sendMail(generatedOtp, email);
logger.info(`📧 OTP sent to: ${email}`);
} catch (error) {
logger.error(`Email sending failed for: ${email}`, error);
}

// Hash password
const hashedPassword = await bcrypt.hash(password, 12);

Expand All @@ -130,6 +123,12 @@ export const registerUser = catchAsync(async (req, res, next) => {
role: role || "student",
});

await enqueue(
"sendOtpEmail",
{ userId: user._id.toString(), otp: generatedOtp },
{ attempts: 5, backoffMs: 1000, idempotencyKey: `otp:${user._id}:${generatedOtp}` }
);

Comment on lines +126 to +131

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether generatedOtp is defined anywhere in authController.js or delegated to a User model method.
rg -n 'generatedOtp' src/controllers/authController.js
rg -n -A5 -B5 'generateOtp|randomInt|otp' src/models/User.js

Repository: Deen-Bridge/dnb-backend

Length of output: 372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- authController.js outline ---'
ast-grep outline src/controllers/authController.js --view expanded || true

echo '--- emailRoutes.js outline ---'
ast-grep outline src/routes/emailRoutes.js --view expanded || true

echo '--- authController.js relevant lines ---'
sed -n '1,180p' src/controllers/authController.js | cat -n

echo '--- emailRoutes.js relevant lines ---'
sed -n '1,220p' src/routes/emailRoutes.js | cat -n

Repository: Deen-Bridge/dnb-backend

Length of output: 9386


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
for path in ["src/controllers/authController.js","src/routes/emailRoutes.js"]:
    p=Path(path)
    print(f"\n## {path} ({sum(1 for _ in p.open())} lines)")
PY

Repository: Deen-Bridge/dnb-backend

Length of output: 252


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## authController.js\n'
sed -n '1,170p' src/controllers/authController.js

printf '\n## emailRoutes.js\n'
sed -n '1,220p' src/routes/emailRoutes.js

Repository: Deen-Bridge/dnb-backend

Length of output: 6517


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- search OTP usage ---'
rg -n "generatedOtp|otp" src | sed -n '1,200p'

echo
echo '--- emailRoutes.js with line numbers ---'
cat -n src/routes/emailRoutes.js

echo
echo '--- authController registerUser region ---'
sed -n '103,135p' src/controllers/authController.js | cat -n

Repository: Deen-Bridge/dnb-backend

Length of output: 3870


Generate a per-registration OTP here. generatedOtp is imported as a module-level constant from src/routes/emailRoutes.js, so every signup reuses the same code until the process restarts. Move OTP creation into registerUser and pass that fresh value to enqueue().

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 131-131: Avoid logging sensitive data
Context: logger.info(✅ User registered successfully: ${email} (ID: ${user._id}))
Note: [CWE-532] Insertion of Sensitive Information into Log File.

(log-sensitive-data)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/controllers/authController.js` around lines 126 - 131, Update
registerUser to generate a fresh OTP for each registration instead of using the
module-level generatedOtp imported from emailRoutes.js. Remove that dependency
in this flow, create the OTP within registerUser, and pass the per-registration
value to the sendOtpEmail enqueue payload and idempotencyKey.

logger.info(`✅ User registered successfully: ${email} (ID: ${user._id})`);

// Generate session and tokens
Expand Down
Loading
Loading