Without trust proxy, the rate limiter uses the reverse proxy IP instead
of the real client IP, making all requests appear to come from one IP.
Set TRUST_PROXY=1 in production when behind a single nginx/Apache hop.
models/mysql/index.js was never imported by the application — mysql.js
is the actual model registry. The barrel created a false registration
point that caused the Application migration bug.
Application was added to models/mysql/index.js but not to mysql.js,
which is the actual file that instantiates all Sequelize models.
DB.Application was undefined, causing the relationships to fail at startup.
A hardcoded fallback 'acapisecret' was silently used when SECRET was
not set, allowing JWT tokens to be forged by anyone knowing the default.
The app now throws at module load time if SECRET is absent.
Helmet adds HTTP security headers (HSTS, X-Frame-Options,
X-Content-Type-Options, etc.). CSP is disabled pending deployment-
specific tuning. Rate limiting (10 req / 15 min per IP) is applied
to POST /login and POST / (registration) to mitigate brute force
and credential stuffing attacks.
Sharing the JWT secret with express-session means a single leaked
secret compromises both auth mechanisms. SESSION_SECRET is now read
from its own env var, falling back to SECRET only if not set.
Mass assignment via Object.assign(entity, req.body) allowed callers
to overwrite protected fields (id, author). Updates are now restricted
to explicitly listed data fields on both Clan and Member.
Wildcard CORS allowed any website to make authenticated cross-origin
requests on behalf of a victim. Origins are now read from the
ALLOWED_ORIGINS env var (comma-separated). No origins configured
means all cross-origin requests are rejected.
Any authenticated user could previously call PUT /update/role and
escalate their own role to Admin. The endpoint now returns 403 if
the requesting user is not already an Admin.
Accepting authorId from the request body allowed any authenticated user
to create comments impersonating another user. The author is now always
the authenticated user from req.payload.
bcrypt with a fixed salt is deterministic but defeats the purpose of
bcrypt salting. API keys are high-entropy UUIDs (128 bits), making
brute-force resistance unnecessary — HMAC-SHA256 with the app secret
is the correct primitive for deterministic, secure key hashing.
Also fixes application.author.id → application.authorId in the API key
auth path, which was silently setting req.payload.id to undefined.
- Add Sequelize migration creating the 'application' table
- Add Application Sequelize model with slugify, toJSONFor, toAuthJSON
- Rewrite passport-headerapikey strategy to use DB.Application
- Rewrite applications.routes.js to use Sequelize (findAll, count,
build/save, destroy) instead of Mongoose
- Fix pre-existing bug: passport was querying { encryptedKey } but the
stored field is named 'apikey'
- Add Application to MySQL models index and relationships
- Remove Application from Mongoose models index and delete the schema
Rewrites all 7 backend ADRs from a custom structure to the MADR 2.1.2
template required by the VS Code ADR Manager extension: bullet metadata
(Status/Date), standardised section headings, "Chosen option: X, because Y"
wording, and explicit Pros/Cons blocks per option.
- Register SkydiverProfile model in mysql.js (fixes startup crash on association)
- Replace $toObjectId with direct string match in all 12 aggregation queries
- Guard toJSONFor on all Mongo models to fall back to MySQL user.toProfileJSONFor()
when author is a UUID string (restores username/image in API responses)
- Pass user to toJSONFor() everywhere instead of null
- Remove dead .populate('author') calls (author: String has no ref)
- Fix ownership check in /last route: compare author string to req.payload.id
instead of the now-absent author.username property
- Extract Express app creation into src/createApp.js to enable testing without starting the server
- Add Jest, Supertest, test helpers (createTestApp, auth token generator)
- Write 14 integration tests for skydive/jumps: auth protection, CRUD, ownership checks
Bugs caught and fixed by the test suite:
- MongoDB models: author field typed as Schema.Types.UUID (Mongoose 6 rejects string UUIDs) → changed to String
- Jump.toJSONFor: called toProfileJSONFor on a UUID string → guarded with typeof check
- exceptions.handler: used err.statusCode but express-jwt throws err.status → returns 500 on 401 errors
- Jump.count(query): deprecated Mongoose method was ignoring the filter → replaced with countDocuments
- jumps.routes GET /: $or query included non-schema field "title" stripped by strictQuery, leaving {} (match-all) → replaced with slug/lieu search
- $regex: was passing a JS RegExp object which serializes to {} in MongoDB → now passes raw string
Following is a CMS concept (article feed by followed authors).
Applying it to jumps was a design error — no component ever called
this endpoint. Removed route and associated UserService.getUserFollowed
dependency from skydive.
All skydive routes now fetch users via UserService (MySQL) instead of
mongoose.model('User'). Key changes:
- user._id.toString() → user.id (same UUID, different source)
- User.findOne({ username }) → UserService.getUserByUsername()
- /feed route migrated to async/await using UserService.getUserFollowed()
to resolve the following list from the MySQL Follower table
- jump/file/application author set as user.id (UUID string) instead of
the full Mongoose document
- jump.toJSONFor(null) — following field in responses is now always false
until Jump model is migrated away from MongoDB User dependency
- user.controller: licence/poids/bg_image routed to SkydiverProfileService
fixing the silent Sequelize save bug for those fields
Introduces the skydiver_profile table as an optional 1-to-1 extension
of the user table, holding skydive-specific fields (licence, poids,
bg_image). Wired up with Sequelize associations and a service with
upsert support. Migration, model, relationships, and service layer
all included.
Migration to MySQL is complete for these three entities — ArticleService,
CommentService, and TagService all use Sequelize exclusively. No remaining
references to the Mongoose versions anywhere in the codebase.
These directories were dead code — commented out in routes/index.js
and superseded by the domain-based structure (skydive/cms/ecommerce/herowars).
Also removes the now-pointless commented-out require lines.
All secrets (DB passwords, API keys, JWT secret) are now sourced
exclusively from config/env/.env.local, which is gitignored.
Nodemon only sets APP_ENV=local so that app.js's dotenv loader
picks up the right env file at boot.
Note: the previously committed values are still in git history.
They must be rotated out-of-band (rotation list tracked separately).
Product and products routes were sitting under /api/cms but conceptually
belong to a separate e-commerce domain. Split them out into a dedicated
ecommerce subrouter mounted at /api/ecommerce.