Skip to main content

User Identity & Logistics Module

The central hub for customer identity, address management, account security, and DPDP‑compliant data governance.

Mongoose Bcrypt Zod Redis BullMQ


1. Executive Summary & Architecture

The User Module (src/modules/users/) is the source of truth for customer identity, logistics (address book), and security credentials. It implements enterprise‑grade protections against mass assignment, session hijacking, and PII leakage, while fully supporting DPDP/GDPR rights (Right to be Forgotten, Data Portability).

Base Route: /api/v1/users

Key architectural decisions:

DecisionImplementationConsequence
Embedded address bookAddressSchema as sub‑document array inside User model.Single DB read fetches profile + all addresses → O(1) performance.
Atomic default address toggleACID transaction demotes previous default when new one is set.No inconsistent state (zero or multiple defaults) even on crash.
Step‑up authenticationPassword change requires OTP (Redis, 10m TTL) + current password hash.Prevents session hijacking; OAuth accounts are blocked.
Memory‑stream avatarsMulter buffer → Cloudinary direct upload (no disk write).Zero server storage, lower latency, CDN delivery.
Saga deletionTransaction across Cart, Wishlist, Orders, Returns, Support + User deletion.Full “Right to be Forgotten” without breaking financial audit trails.

2. API Endpoints (All Private, require JWT)

All routes are prefixed with /api/v1/users and protected by protect middleware.
Rate limiting (standardLimiter) applies globally.

Profile Management

MethodRouteDescriptionValidation
GET/profileFetch authenticated user’s profile + address book
PATCH/profileUpdate demographics (firstname, lastname, phone, gender, dob)UpdateProfileSchema (strict)
POST/profile/avatarUpload avatar (multipart/form-data, single file)Multer validation
DELETE/profileRight to be Forgotten – delete account & anonymize data
POST/profile/exportRequest data export (BullMQ async)

Address Book (Logistics)

MethodRouteDescriptionValidation
POST/profile/addressesAdd new address (auto‑demote default if needed)AddAddressSchema
PATCH/profile/addresses/:addressIdUpdate address fields or toggle defaultUpdateAddressSchema (partial)
DELETE/profile/addresses/:addressIdRemove address; reassign default automatically

Security & Credentials

MethodRouteDescriptionValidation
POST/profile/security/password/otpRequest OTP for password change (email)
PATCH/profile/security/passwordChange password using OTP + current passwordUpdatePasswordSchema

3. Request Flow Sequence Diagrams

3.1 Add Address (with Default Toggle)

3.2 Password Change (Step‑Up Authentication)

3.3 Right to be Forgotten (Account Deletion Saga)


4. Security & Payload Firewalls (Zod)

All incoming payloads are validated using strict Zod schemas. The .strict() modifier rejects any extra field, preventing mass assignment attacks.

UpdateProfileSchema

FieldTypeValidationNotes
firstnamestringmin 2, max 50, optional
lastnamestringmin 2, max 50, optional
phonestringregex ^[6-9]\d{9}$ (Indian 10‑digit)optional, unique
genderenum"MALE" | "FEMALE" | "OTHER"optional
dobstring (ISO 8601).datetime()optional

AddAddressSchema

FieldTypeValidationDefault
streetstringmin 5, max 100required
citystringmin 2, max 50required
statestringmin 2, max 50required
pincodestringregex ^[1-9][0-9]{5}$required
labelenum"HOME" | "WORK" | "OTHER"required
isDefaultbooleanoptionalfalse

UpdateAddressSchema

Same as AddAddressSchema but all fields are .partial() – allows updating a single field.

UpdatePasswordSchema

FieldTypeValidation
otpstringlength 6
currentPasswordstringmin 1
newPasswordstringmin 8, at least one uppercase, one number, one special character

Additional security controls

  • Role hardcoding – No role update exposed at all. Only ADMIN can be assigned via database seed scripts.
  • Log injection preventiondeleteAccount uses safeUserId = String(userId).replace(/[\r\n]/g, "").
  • NoSQL injection – All queries use $eq operator with Mongoose.
  • Password hashingbcrypt with salt rounds = 12, triggered by pre‑save hook.
  • OTP expiry – 10 minutes (600 seconds) TTL in Redis.

5. Database Design (MongoDB)

The User schema is the aggregate root for identity and logistics. It embeds addresses (max 10) to keep read operations optimal.

Schema Highlights

const AddressSchema = {
street: String, city: String, state: String,
pincode: String, label: "HOME"|"WORK"|"OTHER",
isDefault: Boolean
};

const UserSchema = {
authProvider: "LOCAL"|"GOOGLE",
googleId: { type: String, sparse: true, unique: true },
email: { type: String, unique: true, lowercase: true },
password: { type: String, select: false }, // hidden by default
role: "ADMIN"|"USER",
firstname: String, lastname: String,
phone: { type: String, sparse: true, unique: true },
avatar: String (Cloudinary URL),
gender: enum, dob: Date,
addresses: [AddressSchema],
wishlist: [ObjectId] (ref "Product"),
razorpayCustomerId: String,
loyaltyPoints: Number,
preferences: { newsletter: Boolean, smsAlerts: Boolean, privacyPolicyAcceptedAt: Date },
isEmailVerified: Boolean,
isActive: Boolean,
lastLogin: Date
};

Indexes (Performance & Uniqueness)

IndexPurpose
email: uniqueFast authentication lookup.
googleId: unique, sparseAllows multiple null values for local users.
phone: unique, sparseOnly indexed when present.
razorpayCustomerId: sparseOptional vault reference.

Pre‑save Hook (Password Hashing)

UserSchema.pre("save", async function () {
if (!this.isModified("password") || !this.password) return;
const salt = await bcrypt.genSalt(12);
this.password = await bcrypt.hash(this.password, salt);
});

Instance Method

UserSchema.methods.comparePassword = async function (candidatePassword: string): Promise<boolean> {
if (!this.password) return false;
return bcrypt.compare(candidatePassword, this.password);
};

4. Security & Payload Firewalls (Zod)

All incoming payloads are validated using strict Zod schemas. The .strict() modifier rejects any extra field, preventing mass assignment attacks.

UpdateProfileSchema

FieldTypeValidationNotes
firstnamestringmin 2, max 50, optional
lastnamestringmin 2, max 50, optional
phonestringregex ^[6-9]\d{9}$ (Indian 10‑digit)optional, unique
genderenum"MALE" | "FEMALE" | "OTHER"optional
dobstring (ISO 8601).datetime()optional

AddAddressSchema

FieldTypeValidationDefault
streetstringmin 5, max 100required
citystringmin 2, max 50required
statestringmin 2, max 50required
pincodestringregex ^[1-9][0-9]{5}$required
labelenum"HOME" | "WORK" | "OTHER"required
isDefaultbooleanoptionalfalse

UpdateAddressSchema

Same as AddAddressSchema but all fields are .partial() – allows updating a single field.

UpdatePasswordSchema

FieldTypeValidation
otpstringlength 6
currentPasswordstringmin 1
newPasswordstringmin 8, at least one uppercase, one number, one special character

Additional security controls

  • Role hardcoding – No role update exposed at all. Only ADMIN can be assigned via database seed scripts.
  • Log injection preventiondeleteAccount uses safeUserId = String(userId).replace(/[\r\n]/g, "").
  • NoSQL injection – All queries use $eq operator with Mongoose.
  • Password hashingbcrypt with salt rounds = 12, triggered by pre‑save hook.
  • OTP expiry – 10 minutes (600 seconds) TTL in Redis.

5. Database Design (MongoDB)

The User schema is the aggregate root for identity and logistics. It embeds addresses (max 10) to keep read operations optimal.

IndexPurpose
email: uniqueFast authentication lookup.
googleId: unique, sparseAllows multiple null values for local users.
phone: unique, sparseOnly indexed when present.
razorpayCustomerId: sparseOptional vault reference.

6. DPDP / GDPR Compliance (Privacy Engine)

6.1 Right to be Forgotten (Account Deletion Saga)

When a user calls DELETE /profile, the system executes an ACID transaction across multiple modules:

StepModuleAction
1CartdeleteUserCart – wipes ephemeral cart items
2WishlistdeleteUserWishlist – removes all wishlist references
3OrdersanonymizeUserOrders – scrambles PII (name, address) but keeps order line items for tax audit
4ReturnsanonymizeUserReturns – same as orders
5SupportanonymizeUserTickets – nullifies user reference, redacts user messages, destroys attachments
6UserfindOneAndDelete – permanently removes the user document

Result: Business retains historical analytics (orders, tickets) without any personally identifiable information. The user’s refresh token cookie is cleared.

6.2 Right to Access (Data Portability)

POST /profile/export enqueues a job in BullMQ (ExportQueueManager). The worker compiles all user data (profile, orders, returns, tickets) into a JSON file and emails it to the user. The API returns 202 Accepted immediately – non‑blocking.

6.3 Privacy‑Preserving Defaults

  • privacyPolicyAcceptedAt timestamp proves consent.
  • isActive soft‑delete flag allows banning without destroying foreign key references.
  • avatar stored on Cloudinary; can be purged separately.

7. Asynchronous Notifications & Queue Integration

EventNotification MethodQueue
Password update OTPEmail (via NotificationService)None (synchronous, but email is async)
Password change confirmationEmail + In‑app (Bell icon)BullMQ for email, fire‑and‑forget for DB
Data export completionEmail with JSON attachmentBullMQ (ExportQueueManager)

Design principle: Security‑critical alerts (OTP) are sent synchronously to guarantee delivery before API responds. Non‑critical notifications (export) are queued.


8. File Structure Reference

FileResponsibility
user.routes.tsRoute definitions, middleware chain (rate limit, auth, validation, upload)
user.controller.tsHTTP boundary: extracts user ID, calls service, wraps ApiResponse
user.service.tsCore business logic: profile CRUD, address book, password change, deletion saga
user.model.tsMongoose schema, pre‑save hook, comparePassword method
user.interface.tsTypeScript interfaces (IUser, IAddress, IUserPreferences)
dtos/update-profile.dto.tsZod schema for profile updates (strict)
dtos/address.dto.tsZod schemas for add/update address (strict, partial)
dtos/security.dto.tsZod schema for password change (OTP + current + new)
  • src/modules/notifications/notification.service.ts – email and in‑app alerts
  • src/modules/orders/order.service.ts – order anonymization
  • src/modules/returns/return.service.ts – return anonymization
  • src/modules/support/support.service.ts – ticket anonymization
  • src/shared/queues/export.queue.ts – BullMQ data export worker
  • src/config/cloudinary.tsuploadBufferToCloudinary utility
  • src/config/redis.ts – Redis client for OTP storage

9. Security Hardening Summary

ThreatMitigation
Mass assignmentZod .strict() schemas reject extra fields (e.g., role, loyaltyPoints).
Session hijacking (password change)Step‑up authentication: OTP (Redis, 10m) + current password hash.
NoSQL injectionMongoose $eq operator + Zod type coercion.
Log injectionsafeUserId strips CR/LF in deleteAccount.
Password leak via queryselect: false on password field; only explicitly selected with +password.
OAuth password bypasssendPasswordUpdateOtp checks authProvider !== "GOOGLE".
Address book bloatService layer hard cap at 10 addresses per user.
Avatar disk fillDirect memory‑stream to Cloudinary – no local file write.
Default address inconsistencyACID transaction demotes previous default atomically.

See Also


The Reshma-Core Team