Skip to content

Known Issues & Tech Debt

This page tracks known problems and improvement areas. Items are grouped by severity.


File: backend/wrangler.toml

JWT_SECRET and REFRESH_SECRET_KEY are hardcoded in plaintext in a committed file. Anyone with repo read access can forge JWTs for any user. Both the ticket ID obfuscation (which uses JWT_SECRET) and all auth tokens are compromised.

Fix:

Terminal window
# Remove [vars] entries for these two keys from wrangler.toml, then:
npx wrangler secret put JWT_SECRET
npx wrangler secret put REFRESH_SECRET_KEY
# Generate new strong values — the old ones are burned.

File: backend/utils/storage.js

getFileUrl() returns /uploads/filename which is only valid in local dev. In the Cloudflare Worker environment, /uploads/ is not served — it’s a local express.static path that doesn’t exist in Workers.

All uploaded files (KYC docs, submission passports, ticket documents) are inaccessible in production.

Fix: Set R2 bucket to public and configure a custom domain. Update getFileUrl:

const getFileUrl = (filename) => {
if (!filename) return null;
if (globalThis.BUCKET) {
// Production R2 public URL
return `https://uploads.shanvitravel.com.np/${filename}`;
}
return `/uploads/${filename}`; // local dev
};

File: backend/index.js

express-rate-limit is installed but never applied. Login and register endpoints are fully open to brute-force attacks.

Fix: Add rate limiting in index.js:

const rateLimit = require('express-rate-limit');
const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 20, message: { error: 'Too many requests' } });
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);

4. unauthenticated /submit-payment endpoint

Section titled “4. unauthenticated /submit-payment endpoint”

File: backend/routes/submissions.js

Anyone who knows a submission ID (which are sequential integers: 1, 2, 3…) can call /submit-payment on someone else’s submission. The guard that requires status = 'Pending' limits the window, but it’s still exploitable.

Options:

  • Require authentication (breaks the “public payment submission” UX)
  • Issue a one-time token at submission creation time and require it for payment submission
  • At minimum: add rate limiting per IP

5. VFS permissions not enforced server-side

Section titled “5. VFS permissions not enforced server-side”

File: backend/routes/documents.js (and frontend)

VFS task status transitions are only enforced on the frontend (checking userPermissions from the auth context). The backend does not validate whether an agent has the required permission before allowing a status update. This means any agent can update any VFS task to any status by calling the API directly.

Fix: Add permission checks in the VFS update endpoint, mirroring the pattern used in checkCanManageTickets() in tickets.js.


6. Transit detail normalization duplicated

Section titled “6. Transit detail normalization duplicated”

File: backend/routes/tickets.js

The airport lookup + JSON normalization code for transit tickets is copy-pasted between POST / (lines ~322-370) and PUT /:id (lines ~549-596). These two blocks are nearly identical — ~50 lines duplicated.

Fix: Extract to a shared function:

const normalizeTransitDetails = (transitDetails, saleType, airports) => { ... };

File: users.agentType column

The agentType field on users stores the name string (e.g., “VFS Agent”), not the foreign key ID. If an admin renames an agent type, all users assigned to it silently break — their agentType no longer matches any row in agent_types.

Fix: Add agentTypeId INT column to users, migrate existing name references, and use the ID for lookups.


There is no formal staging environment. New features go directly to production. This has caused issues in the past when migrations or changes behave differently on D1 vs. local MySQL.

Fix: Set up a staging Worker + D1 + Cloudflare Pages preview branch (see Deployment docs).


9. Promise wrappers duplicated across route files

Section titled “9. Promise wrappers duplicated across route files”

The all(), run(), get() promise wrappers over the db callback API are copy-pasted in at least routes/tickets.js. They should be in a shared utils/db-helpers.js.


The displayId format 08212xxx is computed from id on every response. If records are deleted and IDs are recycled, display IDs could repeat. Store it as a unique column at creation time.


pages/Dashboard.tsx holds state and fetch logic for every feature tab simultaneously. All data is fetched on mount regardless of which tab is active. Break it up with lazy-loaded tab panels or data fetching hooks per feature.


Schema evolution happens via runtime ALTER TABLE guards scattered through route files. There’s no migration history, no rollback support, and no way to know what schema version a given database is at.

Recommended fix: Adopt Drizzle ORM + Drizzle Kit, which supports both MySQL and SQLite (D1) with a shared schema definition and generates versioned migration SQL files.


Section titled “13. available_dates table is unused by search”

When a ticket is approved, a row is inserted into available_dates. However, the public search and date-picker queries use fd_tickets directly, not available_dates. The table is dead code. Also, the INSERT IGNORE syntax used is MySQL-specific and fails silently on D1.


14. getFileUrl returns inconsistent format

Section titled “14. getFileUrl returns inconsistent format”

For local files, getFileUrl returns /uploads/kyc/filename.jpg. For this to resolve, the path must be uploads/kyc/filename.jpg relative to __dirname. Currently the file is saved to uploads/folder/uniqueName but the key returned is folder/uniqueName. The /uploads/ prefix is prepended by getFileUrl. This is internally consistent but fragile — any caller who stores the raw key and constructs their own URL will get it wrong.