Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
903436e
feat(stellar): add path payment support with quote endpoint and slipp…
abimbolaalabi Jul 21, 2026
78c4c61
Merge branch 'main' into feat/path-payment
abimbolaalabi Jul 21, 2026
77543bb
fix: resolve duplicate export and add asset issuer validation
abimbolaalabi Jul 21, 2026
ca44979
fix: resolve rebase conflict artifacts in paymentController.js
abimbolaalabi Jul 21, 2026
8cfa18e
Merge pull request #40 from abimbolaalabi/feat/path-payment
zeemscript Jul 21, 2026
ba285b7
feat(jobs): add durable background processing
Dayz-tech-co Jul 21, 2026
2866f7b
Merge pull request #41 from Dayz-tech-co/feat/32-background-jobs
zeemscript Jul 21, 2026
491c734
feat: add pre-flight payment safety checks with SEP-29 memo detection
Tijesunimi004 Jul 23, 2026
768029d
Merge pull request #68 from Tijesunimi004/feat/preflight-payment-safe…
zeemscript Jul 23, 2026
0146810
implemented non-custodial refund and dispute flow (#62)
Ahbiz Jul 23, 2026
9450672
chore: add retrying to Transaction status enum
Ahbiz Jul 23, 2026
7dcaa36
chore: resolve merge conflict with upstream/dev (keep refunded/disput…
Ahbiz Jul 23, 2026
ec82ab6
fix: remove Mongoose session/transaction from submitRefund for standa…
Ahbiz Jul 23, 2026
bd0487a
Merge pull request #69 from Ahbiz/feature/non-custodial-refunds
zeemscript Jul 23, 2026
2799ef6
feat: cache POST-based recommendation endpoints and document cache layer
Tijesunimi004 Jul 24, 2026
8a841fe
Merge branch 'dev' into feat/19-wire-up-redis-cache-layer
Tijesunimi004 Jul 24, 2026
769e0c9
fix: repair stellarPaymentController mock drift and ESM export mismatch
Tijesunimi004 Jul 24, 2026
65229da
Merge pull request #71 from Tijesunimi004/feat/19-wire-up-redis-cache…
zeemscript Jul 24, 2026
cae9ec3
feat: media upload hardening with limits, magic bytes, and signed del…
Kaycee276 Jul 24, 2026
e276d78
Merge branch 'dev' into fix/67-media-upload-hardening
Kaycee276 Jul 24, 2026
2138e57
Merge pull request #73 from Kaycee276/fix/67-media-upload-hardening
zeemscript Jul 24, 2026
ac2b1bb
feat(search): add full-text search API with text indexes, ranking, fi…
Kaycee276 Jul 24, 2026
af64f16
feat(stellar): publish SEP-1 stellar.toml metadata endpoint at /.well…
Ahbiz Jul 24, 2026
c0811db
fix(stellar): address CodeRabbit review feedback for SEP-1 stellar.toml
Ahbiz Jul 24, 2026
ecbeddd
Merge pull request #75 from Ahbiz/feature/sep1-stellar-toml
zeemscript Jul 24, 2026
4291c51
fix: env-aware refresh token cookie settings for local dev
zeemscript Jul 25, 2026
a6d5826
feat(notifications): wire producers, batched fan-out, redis pubsub, p…
sublime247 Jul 25, 2026
55b9551
Merge branch 'dev' into dev
sublime247 Jul 25, 2026
2827f8a
fix(notifications): restore notification controller imports in book, …
sublime247 Jul 25, 2026
d4ed338
feat(auth): align canonical role enum, authorize middleware, anti-esc…
sublime247 Jul 25, 2026
57c9c27
Merge pull request #77 from sublime247/feature/roles-and-authorization
zeemscript Jul 25, 2026
0b34c14
refactor: standardize API response structure, improve search service …
Kaycee276 Jul 25, 2026
bb1f3be
Merge branch 'dev' into feat/64-full-text-search
Kaycee276 Jul 25, 2026
45c5382
refactor: centralize test environment configuration and add authoriza…
Kaycee276 Jul 25, 2026
df10742
Merge remote-tracking branch 'origin/feat/64-full-text-search' into f…
Kaycee276 Jul 25, 2026
f3d5e9f
refactor: use stored testUserId in upload.test.js to simplify user up…
Kaycee276 Jul 25, 2026
1f673ff
chore: implement dynamic environment variable loading from .env.test …
Kaycee276 Jul 25, 2026
f85f1df
refactor: use cloudinary configuration object instead of process.env …
Kaycee276 Jul 25, 2026
f6db82f
fix: add cloudinary test env variables to CI and strict assertion
Kaycee276 Jul 25, 2026
f22bdb8
Merge pull request #74 from Kaycee276/feat/64-full-text-search
zeemscript Jul 25, 2026
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
21 changes: 21 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ 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
Comment on lines +29 to +33

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

Remove the duplicate EmailJS entries.

These keys already exist at Lines 22-26. Because dotenv resolves duplicate keys by the later value, editing the first EmailJS block will be silently overridden by these placeholders, causing mail configuration failures. Keep only one EmailJS block and add EMAILJS_RECEIPT_TEMPLATE_ID beside the existing template ID.

🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 29-29: [DuplicatedKey] The EMAILJS_PRIVATE_KEY key is duplicated

(DuplicatedKey)


[warning] 30-30: [DuplicatedKey] The EMAILJS_PUBLIC_KEY key is duplicated

(DuplicatedKey)


[warning] 31-31: [DuplicatedKey] The EMAILJS_SERVICE_ID key is duplicated

(DuplicatedKey)


[warning] 32-32: [DuplicatedKey] The EMAILJS_TEMPLATE_ID key is duplicated

(DuplicatedKey)


[warning] 33-33: [UnorderedKey] The EMAILJS_RECEIPT_TEMPLATE_ID key should go before the EMAILJS_SERVICE_ID key

(UnorderedKey)

🤖 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 @.env.example around lines 29 - 33, Remove the duplicate EmailJS variable
block near the end of .env.example, retain the existing block, and add
EMAILJS_RECEIPT_TEMPLATE_ID beside its existing EMAILJS_TEMPLATE_ID entry so
each key appears only once.

Sources: Path instructions, Linters/SAST tools

STELLAR_NETWORK=testnet

# Resilient Horizon Client Configuration (Optional)
Expand All @@ -42,6 +47,17 @@ DONATION_WALLET_PUBLIC_KEY=GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
PLATFORM_FEE_PERCENT=0
PLATFORM_WALLET_PUBLIC_KEY=GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

# SEP-1 stellar.toml (/.well-known/stellar.toml) — optional, omitted when blank
STELLAR_PLATFORM_PUBLIC_KEY=
ORG_NAME=
ORG_URL=
ORG_DESCRIPTION=
ORG_LOGO=
ORG_TWITTER=
ORG_GITHUB=
# SEP-10 signing key (leave blank until #25 is implemented)
SIGNING_KEY=

# Token TTLs
ACCESS_TOKEN_TTL=15m
REFRESH_TOKEN_TTL=30d
Expand All @@ -63,3 +79,8 @@ REDIS_PORT=6379
# JITSI_PUBLIC_KEY_ID=your_public_key_id
# JITSI_KID=your_kid
# JITSI_TENANT=your_tenant
# Durable Mongo-backed background jobs (tests use inline)
JOBS_ENABLED=true
QUEUE_DRIVER=mongo
JOBS_DASHBOARD_TOKEN=replace_with_a_long_random_token

6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ jobs:
MONGO_URI: mongodb://localhost:27017/deenbridge_test
JWT_SECRET: test-secret-key-for-ci-minimum-32-chars
STELLAR_NETWORK: testnet
CLOUDINARY_CLOUD_NAME: ci_test_cloud
CLOUDINARY_API_KEY: ci_test_key
CLOUDINARY_API_SECRET: ci_test_secret_that_should_not_leak

build:
name: Syntax and Boot Check
Expand Down Expand Up @@ -88,3 +91,6 @@ jobs:
MONGO_URI: mongodb://localhost:27017/deenbridge_boot
JWT_SECRET: test-secret-key-for-ci-minimum-32-chars
STELLAR_NETWORK: testnet
CLOUDINARY_CLOUD_NAME: ci_test_cloud
CLOUDINARY_API_KEY: ci_test_key
CLOUDINARY_API_SECRET: ci_test_secret_that_should_not_leak
63 changes: 36 additions & 27 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,27 +1,36 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Dependency directories
node_modules/
jspm_packages/

# Environment variables
.env

# Build output
dist/
build/

# IDE files
.vscode/
.DS_Store
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Dependency directories
node_modules/
jspm_packages/

# Environment variables
.env
.env.test

# Build output
dist/
build/

# IDE files
.vscode/
.DS_Store

# Spec & Agent temporary docs
Tasks.md
Memory.md
Handoff.md
PRD.md
Architecture.md
scratch/
12 changes: 12 additions & 0 deletions QUICK_START.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ curl -X POST http://localhost:5000/api/auth/login \

---

## 🌐 **Stellar TOML (SEP-1)**

The API serves ecosystem metadata at `/.well-known/stellar.toml`:

```bash
curl http://localhost:5000/.well-known/stellar.toml
```

Optional env vars for the TOML: `STELLAR_PLATFORM_PUBLIC_KEY`, `ORG_NAME`, `ORG_URL`, `ORG_DESCRIPTION`, `ORG_LOGO`, `ORG_TWITTER`, `ORG_GITHUB`. (`SIGNING_KEY` is a placeholder that must remain unset until SEP-10 #25). See `.env.example`.

---

## 🔍 **Monitor Logs**

```bash
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,23 @@ 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` |
| `STELLAR_PLATFORM_PUBLIC_KEY` | Public key published in `stellar.toml` `ACCOUNTS[]` |

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 @@ -94,6 +108,7 @@ See `.env.example` for the full list.

| Area | Base Route |
|------|-----------|
| SEP-1 Metadata | `/.well-known/stellar.toml` |
| Auth & Users | `/api/auth`, `/api/users` |
| Courses & Books | `/api/courses`, `/api/books` |
| Spaces & Reels | `/api/spaces`, `/api/reels` |
Expand Down
9 changes: 9 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,9 @@ 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 uploadRoutes from "./src/routes/uploadRoutes.js";
import jobsRoutes from "./src/routes/jobsRoutes.js";
import wellKnownRoutes from "./src/routes/wellKnownRoutes.js";

handleUncaughtException();
validateEnv();
Expand Down Expand Up @@ -159,6 +163,9 @@ app.get("/health", (req, res) => {
});
});

// SEP-1 stellar.toml — must be outside /api rate limiter
app.use("/.well-known", wellKnownRoutes);

app.use("/api", apiLimiter);

// Auth routes
Expand All @@ -179,6 +186,8 @@ 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("/api/uploads", uploadRoutes);
app.use("/admin/jobs", jobsRoutes);

// ======================
// ERROR HANDLING
Expand Down
75 changes: 75 additions & 0 deletions docs/caching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Redis Caching

This documents which endpoints are cached, their TTLs, and the invalidation
strategy. The underlying helpers live in `src/utils/cache.js` (Redis
primitives, `CACHE_TTL`/`CACHE_KEYS` presets) and `src/middlewares/cache.js`
(`cacheMiddleware`, `smartCache`, `invalidateCacheMiddleware`).

All caching degrades gracefully: `getCache`/`setCache`/`deleteCachePattern`
check `isRedisReady()` first and no-op (falling through to Mongo) when Redis
is unavailable, so a down Redis instance never breaks a request.

## Cached read endpoints

| Endpoint | Cache key | TTL | Notes |
|---|---|---|---|
| `GET /api/books` | `books:list` | 15 min (`CACHE_TTL.BOOKS`) | |
| `GET /api/books/:id` | `book:<id>` | 15 min | |
| `GET /api/books/by-author/:authorId` | `books:author:<authorId>` | 15 min | |
| `GET /api/books/recom` | `books:recommended` | 3 min (`CACHE_TTL.SHORT`) | shared key regardless of `interests`; the controller (`fetchRecommendedBooks`) also has a pre-existing bug (undefined `category` reference) that makes it non-functional today - out of scope here |
| `POST /api/books/recommended` | `books:recommended:<sorted interests \| "popular">` | 15 min (`CACHE_TTL.BOOKS`) | added in this change; keyed explicitly in the controller since `cacheMiddleware` only intercepts GET |
| `GET /api/courses` | `courses:list` | 15 min (`CACHE_TTL.COURSES`) | |
| `GET /api/courses/:id` | `course:<id>` | 15 min | |
| `GET /api/courses/user` | `courses:user:<createdBy>` | 15 min | |
| `POST /api/courses/recommended` | `courses:recommended:<sorted interests \| "none">` | 15 min (`CACHE_TTL.COURSES`) | added in this change; previously explicitly left uncached ("POST routes not cached") |
| `GET /api/spaces` | `spaces:list` | 5 min (`CACHE_TTL.SPACES`) | |
| `GET /api/spaces/:id` | `space:<id>` | 5 min | |
| `GET /api/spaces/by-host/:hostId` | `spaces:host:<hostId>` | 5 min | |
| `GET /api/search` | `search:<type>:<q>` | 5 min (`CACHE_TTL.SEARCH`) | |
| `GET /api/users/:id` | `user:<id>` | 10 min (`CACHE_TTL.USERS`) | |
| `GET /api/users/recommendations` | `user:<requester id>:recommendations` | 10 min | keyed per requesting user |
| `GET /api/users/:userId/followers` | `user:<userId>:followers` | 10 min | |
| `GET /api/users/:userId/following` | `user:<userId>:following` | 10 min | |

The books/courses/spaces/search/users list and detail endpoints above were
wired up separately (see `feat: configure Redis cache layer for production
use`, merged to `main`). This change adds the two `POST /recommended`
entries, which that work left uncached, plus this document.

## Invalidation

Writes call `invalidateCacheMiddleware` with wildcard patterns so a create,
update, delete, or review clears the relevant cached list/detail entries
(`deleteCachePattern` runs a Redis `KEYS` scan against the pattern):

- Books: `createBook` invalidates `books:*`; `deleteBook` and `addBookReview` invalidate `books:*` and `book:*`.
- Courses: `createCourse` invalidates `courses:*`; `enrollInCourse`/`addCourseReview` invalidate `course:*`; `updateCourse` invalidates `courses:*` and `course:*`.
- Spaces: `createSpace` invalidates `spaces:*`; `joinWaitList` invalidates `space:*`; `updateSpace`/`deleteSpace` invalidate `spaces:*` and `space:*`.
- Users: profile/follow writes invalidate `user:*` (and `user:*:followers` / `user:*:following` on follow/unfollow).

`books:*` and `courses:*` also cover the new `books:recommended:*` /
`courses:recommended:*` keys added in this change, since they share the same
prefix - no separate invalidation wiring was needed for them.

Search has no dedicated invalidation hook (it spans five collections); its
5-minute TTL is the staleness bound instead.

## Deliberately not cached

- **Bookmarks** (`GET /api/books/bookmarks`, `GET /api/courses/bookmarks`,
and the `bookmark/check` routes) are per-user and are left uncached rather
than risk leaking one user's bookmark state into another user's response
under a shared key.
- **Reels** (`GET /api/reels`, `GET /api/reels/:id`) embed the requesting
user's `viewerState` (liked/loved) directly in each reel object. Caching
the response as-is would leak one viewer's like/love state to every other
viewer who hits the same cache key, so these are left uncached until the
response shape separates viewer-specific state from the shared reel data.

## User-specific data

`cacheMiddleware`/`smartCache` support a per-request key generator (and
`smartCache`'s `includeUser` option) for endpoints that must vary per user -
see `GET /api/users/recommendations` above, which folds the requester's id
into the key. Any future authenticated, user-specific GET endpoint should do
the same rather than caching under a shared key.
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export default {
testEnvironment: "node",
transform: {},
setupFiles: ["<rootDir>/test/jest.setup.js"],
};
Loading
Loading