Known Issues & Tech Debt
This page tracks known problems and improvement areas. Items are grouped by severity.
🔴 Critical (fix before next release)
Section titled “🔴 Critical (fix before next release)”1. JWT secrets committed in wrangler.toml
Section titled “1. JWT secrets committed in wrangler.toml”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:
# Remove [vars] entries for these two keys from wrangler.toml, then:npx wrangler secret put JWT_SECRETnpx wrangler secret put REFRESH_SECRET_KEY# Generate new strong values — the old ones are burned.2. File URLs broken in production (R2)
Section titled “2. File URLs broken in production (R2)”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};3. No rate limiting on auth endpoints
Section titled “3. No rate limiting on auth endpoints”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);🟡 High (should fix soon)
Section titled “🟡 High (should fix soon)”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) => { ... };7. Agent type stored by name (not ID)
Section titled “7. Agent type stored by name (not ID)”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.
8. No staging environment
Section titled “8. No staging environment”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).
🟢 Low (tech debt, fix when convenient)
Section titled “🟢 Low (tech debt, fix when convenient)”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.
10. displayId is computed, not stored
Section titled “10. displayId is computed, not stored”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.
11. Dashboard.tsx is too large
Section titled “11. Dashboard.tsx is too large”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.
12. No database migration tool
Section titled “12. No database migration tool”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.
13. available_dates table is unused by search
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.