Auth Hardening, Localization, and a Few Hard Lessons

What I Did

Authentication completeness. Added the missing pieces of the auth flow: email verification via OTP, resend-with-cooldown, and a forgot-password/reset-password cycle. Successful password resets now revoke every active refresh-token session — a deliberate security choice, since a password reset should invalidate any device that was logged in under the old credentials.

API/frontend contract alignment. The backend’s supplier endpoint was returning camelCase fields, but the frontend model expected the same — turned out the two had drifted at some point, and a chunk of the week went into re-syncing them (company name, contact name, balance, payment status) so the supplier table actually rendered.

Retailer profile as its own service. Profile data (name, email, phone, shop name, shop phone, shop logo) was originally handled inside user.service.js. Split it into a dedicated profile.service.js so authentication logic and profile logic stop being tangled together — cleaner separation, easier to extend later.

Shop logo handling. Built create/replace/delete for the shop logo, with images normalized to a 40×40 WebP thumbnail server-side and capped at 2MB on upload — keeps storage predictable regardless of what the retailer uploads.

Localization (i18n). Added i18n.js, five locale files (English, Urdu, Arabic, Spanish, Chinese), and wired translations into the retailer dashboard — printer setup, store settings, category cards, and their action buttons. User-generated content (a retailer’s own category names) intentionally stays untranslated, since that’s their data, not UI chrome.

Removed product variants from the frontend. Pulled variant lists, per-variant stock display, and the “Add Variant” modal out of the product cards and inventory flow entirely, matching a backend decision to drop variants from the product model.

Issues & Challenges (and how they got resolved)

Silent naming drift between backend and frontend. The supplier fields were a symptom of a bigger risk: nothing forces the two sides to agree on camelCase vs snake_case except discipline. Fixed it this time by aligning the API response, but the real fix is treating this as a contract worth documenting explicitly (Swagger schemas help, but only if kept current).

SMTP silently failing. Emails weren’t sending, and the fix wasn’t obvious at first — Unexpected socket close from Nodemailer usually just means “something about the SMTP handshake didn’t complete,” which could be host, port, TLS mode, or credentials. Traced it to two things: an incorrectly quoted SMTP_FROM value (only the display name was inside quotes, not the full “Name <email>” string) and a Gmail-specific requirement — an App Password, not the regular account password, plus matching port/secure-flag combinations (587/false or 465/true).

A ReferenceError from a missing import. The category-wise product endpoint crashed with tables is not defined — a straightforward missing import { tables } from ‘../../../config/schema.js’ in catalog.service.js. Easy fix, but a reminder that schema-qualified table references (tables.categories, etc.) create a hard dependency on that import existing in every file that uses them — miss it once, and it’s a runtime crash, not a caught-at-write-time error.

Destructive schema reset. Needed to wipe and rebuild the iq-pos schema from scratch at one point. Handled carefully: dropped only the schema (DROP SCHEMA “iq-pos” CASCADE), not the whole database, then let migrate:up rebuild every table from the migration history — a good demonstration of why migrations exist: the schema was fully reconstructable from version-controlled files, no manual re-creation needed.

Removing a feature without breaking a dependent one. Pulling variants off the frontend surfaced a real coupling: POS still sends productVariantId when adding a bill item. Variants disappearing from the product UI doesn’t mean the backend billing contract changed — so POS integration is intentionally paused until the backend confirms what the new bill-item payload should look like (likely productId directly). Better to flag the dependency than silently break checkout.

What I Learned

  • Contract drift between frontend and backend is a recurring risk, not a one-time bug — the supplier field mismatch and the variant-removal situation are the same root problem wearing different clothes: two sides evolving independently without a single source of truth for the shape of data between them.
  • Security-sensitive defaults matter more than they seem at first glance — revoking all refresh tokens on password reset, generic “if the account exists” messaging on forgot-password, and OTP length/expiry rules are all small decisions that meaningfully reduce attack surface.
  • Destructive database operations deserve a deliberate, confirmed process — schema-only drops (never whole-database), and leaning on migrations as the recovery mechanism, turns a risky operation into a reproducible one.
  • Splitting services by concern pays off almost immediately — moving profile logic out of the auth service wasn’t just tidiness; it made the profile endpoints easier to document, test, and reason about independently.

Author: Muhammad Faisal

Leave a Reply

Your email address will not be published. Required fields are marked *