Skip to content

API Documentation Review Findings

Reviewed: docs/architecture/api.md (Generated 2026-02-12) Review date: 2026-02-25 Method: Adversarial code-level audit — every finding verified against actual route files, controllers, services, validation schemas, and TypeORM entities Total findings: 70 Status: ALL 70 RESOLVED — api.md completely rewritten on 2026-02-25 to match actual codebase


Severity Legend

Tag Meaning
CRITICAL Endpoint does not exist, path is fundamentally wrong, or auth is completely inverted
HIGH Request/response shapes wrong (clients will break), missing endpoints, security holes
MEDIUM Functional inaccuracies, undocumented fields, misleading descriptions
LOW Minor naming bugs, code quality, cosmetic issues

Category A: Fictional / Nonexistent Endpoints (10 findings)

These endpoints are documented in api.md but do not exist in the codebase at all.

A-1. POST /payments/checkout does not exist [CRITICAL]

  • Doc ref: api.md lines 426-443
  • Verified in: saas-boilerplate/src/app/modules/v1/Payment/payment.route.ts — only 2 routes: GET / and POST /stripe/customer-portal-session
  • Actual checkout path: POST /api/v1/subscriptions/subscribe (in Subscription/subscription.routes.ts line 13-18)
  • Impact: Any client built against this endpoint gets a 404

A-2. GET /payments/verify/:sessionId does not exist [CRITICAL]

  • Doc ref: api.md lines 445-447
  • Verified in: payment.route.ts — no verify route. StripeService.verify() exists internally but has no route handler
  • Impact: 404 for all callers

A-3. GET /routes does not exist [CRITICAL]

  • Doc ref: api.md lines 888-891
  • Verified in: src/app/routes/v1/index.ts — no /routes path in the moduleRoutes array. No Routes module exists anywhere under src/app/modules/v1/
  • Impact: Entirely fabricated endpoint

A-4. GET /public/navigation-links/:slug does not exist [CRITICAL]

  • Doc ref: api.md line 786-787
  • Verified in: src/app/modules/v1/Public/public.routes.ts — the 8 actual routes are: /blogs, /menus, /menu-items, /home-page-blogs, /top-blogs, /blogs/:categoryNameOrSlug/featured, /blogs/featured-categories, /blogs/:id
  • Impact: 404. No navigation-links concept exists in Public module

A-5. GET /public/settings/:group does not exist [CRITICAL]

  • Doc ref: api.md line 789-790
  • Verified in: public.routes.ts — no settings-related route exists in the Public module
  • Impact: 404

A-6. DELETE /uploads/:publicId does not exist [CRITICAL]

  • Doc ref: api.md lines 752-753
  • Verified in: src/app/modules/v1/Image/image.route.ts — single route only: POST /image
  • Impact: 404

A-7. GET /settings/group/:group does not exist [CRITICAL]

  • Doc ref: api.md line 679-680
  • Verified in: src/app/modules/v1/Setting/setting.routes.ts — only GET /, GET /:prefix/prefix, and GET /:id exist. No group-based route.
  • Impact: 404

A-8. POST /uploads path is wrong — actual: POST /uploads/image [CRITICAL]

  • Doc ref: api.md lines 739-740
  • Verified in: image.route.ts line 9: router.post("/image", ...) mounted at /uploads in index.ts line 117-118
  • Also: Route has NO auth() middleware despite doc claiming "Auth required"
  • Impact: 404 at documented path; upload is unauthenticated

A-9. GET /settings/admin/prefix path is wrong — actual: GET /settings/:prefix/prefix [CRITICAL]

  • Doc ref: api.md line 677
  • Verified in: setting.routes.ts line 11: router.get("/:prefix/prefix", ...):prefix is a dynamic parameter, not literal admin
  • Impact: Misleading, though calling /settings/admin/prefix would happen to match the parameterized route

A-10. GET /analytics/dashboard path is wrong — actual: GET /analytics/ [HIGH]

  • Doc ref: api.md line 841
  • Verified in: src/app/modules/v1/Analytic/analytic.routes.ts line 12: router.get("/", ...) mounted at /analytics in index.ts line 45
  • Impact: 404 at /analytics/dashboard

Category B: Auth / Access Level Completely Wrong (5 findings)

Endpoints where the doc says "Public" but code requires authentication, or vice versa.

B-1. GET /blogs is NOT public — requires auth [CRITICAL]

  • Doc ref: api.md line 459: "List blogs. Public."
  • Verified in: blog.routes.ts lines 11-16: router.get("/", auth(), contextMiddleware, sanitizeQuery, BlogController.getAllBlogs)auth() applied before handler
  • Impact: Unauthenticated clients get 401

B-2. GET /blog-categories is NOT public — requires auth [CRITICAL]

  • Doc ref: api.md line 525: "List categories. Public."
  • Verified in: blog_category.routes.ts line 12: router.use(auth(), contextMiddleware) before all routes including GET / at line 14
  • Impact: Unauthenticated clients get 401

B-3. GET /menus and GET /menus/:id are NOT public — require auth [CRITICAL]

  • Doc ref: api.md lines 621, 624: both marked "Public."
  • Verified in: menu.routes.ts line 9: router.use(auth(), contextMiddleware) before all routes
  • Impact: Unauthenticated clients get 401 on both endpoints

B-4. GET /product-categories/all is NOT public — requires auth [CRITICAL]

  • Doc ref: api.md line 585: "Get all product categories (no pagination). Public."
  • Verified in: product_category.routes.tsrouter.use(auth(), contextMiddleware) applied before all routes
  • Impact: Unauthenticated clients get 401

B-5. GET /settings, GET /settings/:prefix/prefix, GET /settings/:id are actually PUBLIC [HIGH]

  • Doc ref: api.md line 674: "List all settings. Auth + Permission required."
  • Verified in: setting.routes.ts lines 10-13: all three GET routes registered BEFORE router.use(auth(), contextMiddleware) at line 13
  • Impact: Settings data (potentially sensitive) exposed without auth. Doc incorrectly claims protection.

Category C: Wrong Request/Response Shapes (17 findings)

Field names, body structures, or response shapes that differ between doc and code.

C-1. POST /auth/refresh-token reads from cookies, NOT request body [HIGH]

  • Doc ref: api.md lines 199-204: body { "refreshToken": "..." }
  • Verified in: auth.contoroller.ts line 277: const { refresh_token } = req.cookies;
  • Impact: Client sends body, server ignores it and reads cookie. Silent failure.

C-2. /auth/login rate limit values completely wrong [HIGH]

  • Doc ref: api.md line 73: "Rate limited: 100 requests per 15 minutes"
  • Verified in: auth.route.ts lines 20-22: windowMs: 1 * 60 * 1000 (1 minute), max: 5. Code comments also wrong (say "15 minutes" and "3 requests").
  • Actual: 5 requests per 1 minute per IP

C-3. POST /auth/verify-backup-code field name wrong [HIGH]

  • Doc ref: api.md lines 138-141: { "backupCode": "ABCD-1234-EFGH" }
  • Verified in: auth.validation.ts lines 70-74: field is code, not backupCode
  • Impact: Validation error for clients following docs

C-4. POST /roles request body — all field names wrong [HIGH]

  • Doc ref: api.md lines 334-338: { "name": "editor", "permissions": [1, 2, 3] }
  • Verified in: role.validation.ts lines 5-22: expects { "role": "...", "displayName": "...", "permissionIds": [...] }
  • Impact: Three field name mismatches — name->role, permissions->permissionIds, displayName missing

C-5. POST /users request body field name wrong [HIGH]

  • Doc ref: api.md lines 280-287: uses roleId
  • Verified in: user.validation.ts: field is role (number), not roleId. Also missing: userRoles (array), phone, address, isInviteOnly

C-6. PATCH /users/bulk-delete body field wrong [HIGH]

  • Doc ref: api.md lines 305-309: { "ids": [1, 2, 3] }
  • Verified in: user.validation.ts lines 27-31: expects { "userIds": [...] }, controller line 167 reads req.body.userIds

C-7. POST /permissions request body field wrong [HIGH]

  • Doc ref: api.md lines 355-360: { "route": "/custom-feature" }
  • Verified in: permission.validation.ts lines 4-10: field is name, not route. Service maps payload.name to route column.

C-8. Login 2FA email response shape wrong [HIGH]

  • Doc ref: api.md lines 104-108: { "token": null, "user": null, "method": "email" }
  • Verified in: auth.contoroller.ts lines 110-115: returns { method: "otp_verification" } only — no token or user fields

C-9. Login 2FA Google method string wrong [MEDIUM]

  • Doc ref: api.md line 108: "google_authenticator"
  • Verified in: auth.contoroller.ts line 124: returns { method: "google_auth" }

C-10. POST /menu-items body is NOT a bare array [HIGH]

  • Doc ref: api.md lines 644-661: shows [{...}, {...}]
  • Verified in: menuItem.validation.ts lines 26-35: expects { "items": [{...}] } — array wrapped in items property

C-11. Blog POST body — 6+ field mismatches [HIGH]

  • Doc ref: api.md lines 476-488
  • Verified in: blog.validation.ts lines 27-46
  • slug NOT in validation — not accepted on create
  • categoryId should be categories (array of numbers — ManyToMany relation)
  • imageUrl is via upload.single("image") file upload, not a JSON field
  • Content-Type is multipart/form-data, not application/json
  • authorName required but undocumented
  • 6 undocumented fields: imageAlt, date, isTrending, isFeatured, showOnSlider, metaKeywords

C-12. GET /subscriptions/my path wrong — actual: /subscriptions/me [HIGH]

  • Doc ref: api.md line 418-419
  • Verified in: subscription.routes.ts line 12: router.get("/me", ...)
  • Also: Uses authGuard("user") — only "user" role allowed, admins get 403

C-13. Checkout response key checkoutUrl wrong — actual: url [MEDIUM]

  • Doc ref: api.md lines 439-442: { "checkoutUrl": "..." }
  • Verified in: payment.service.ts lines 128-130: returns { url: session.checkoutUrl, sessionId: session.sessionId }

C-14. GET /users/my-permissions response shape wrong [HIGH]

  • Doc ref: api.md lines 248-250: { "permissions": ["/dashboard", "/blogs"] }
  • Verified in: Response is a flat array in data (not wrapped in permissions key). Permission format is dot-notation "user.view", not URL-path style.

C-15. GET /users query params wrong [HIGH]

  • Doc ref: api.md lines 268, 271: search, isDeleted
  • Verified in: user.service.ts: actual params are searchTerm (not search) and trashOnly (not isDeleted)

C-16. Package query param billingCycle wrong — actual: billingType [MEDIUM]

  • Doc ref: api.md line 370
  • Verified in: package.service.ts line 75: queries by billingType

C-17. Paginated response meta missing totalPages [MEDIUM]

  • Doc ref: api.md lines 35-41: shows { page, limit, total, totalPages }
  • Verified in: sendResponse.ts lines 7-10: type only has page, limit, total — no totalPages

Category D: Missing / Undocumented Endpoints (12 findings)

Entire endpoint groups that exist in code but are absent from api.md.

D-1. 12 of 14 Permission endpoints undocumented [CRITICAL]

  • Doc ref: api.md lines 347-361 — only documents GET / and POST /
  • Verified in: permission.routes.ts — 14 total routes
  • Missing: GET /all, GET /:permissionId, GET /user-permissions, POST /check, PATCH /bulk-delete, PATCH /bulk-restore, PATCH /:permissionId, PUT /grant, DELETE /delete, DELETE /:permissionId, PATCH /:permissionId/restore, DELETE /:permissionId/permanent

D-2. 4 Role endpoints undocumented [HIGH]

  • Verified in: role.routes.ts
  • Missing: PATCH /bulk-delete, PATCH /bulk-restore, PATCH /:id/restore, DELETE /:id/permanent

D-3. 6 Menu Item endpoints undocumented [HIGH]

  • Verified in: menuItem.routes.ts — 10 total routes, doc shows 4
  • Missing: GET /:id, PATCH /bulk-update, PATCH /bulk-delete, PATCH /bulk-restore, PATCH /:id/restore, DELETE /:id/permanent

D-4. 5 Menu endpoints undocumented [HIGH]

  • Verified in: menu.routes.ts — 9 total routes, doc shows 5 (with wrong path for GET by ID)
  • Missing: PATCH /bulk-delete, PATCH /bulk-restore, PATCH /:id/restore, DELETE /:id/permanent

D-5. 6 Page endpoints undocumented [HIGH]

  • Verified in: generic-page.routes.ts — 10 total routes, doc shows 5
  • Missing: POST /bulk-permanent-delete, PATCH /bulk-delete, PATCH /bulk-restore, PATCH /:id/restore, DELETE /:id/permanent

D-6. 7 Blog Category endpoints undocumented [HIGH]

  • Verified in: blog_category.routes.ts — 10 total routes, doc shows 4
  • Missing: GET /all, GET /:id, PATCH /:id/restore, PATCH /bulk-delete, PATCH /bulk-restore, DELETE /:id/permanent

D-7. 4 Package endpoints undocumented [HIGH]

  • Verified in: package.routes.ts
  • Missing: PATCH /bulk-delete, PATCH /bulk-restore, PATCH /:id/restore, DELETE /:id/permanent
  • Also: PATCH /:id/restore registered twice (lines 42-45 and 47-50) — dead duplicate

D-8. 8 Invite User endpoints undocumented [HIGH]

  • Verified in: invite-user.route.ts — 10 total routes, doc shows 3
  • Missing: POST /check-invite (public!), GET /:id, PATCH /:id/restore, PATCH /bulk-delete, PATCH /bulk-restore, PATCH /:id, DELETE /:id/permanent
  • Also: Routes mounted at BOTH /invite-users AND /user-invites (index.ts lines 141-146)

D-9. 7 Contact endpoints undocumented [HIGH]

  • Verified in: contact.route.ts — 10 total routes, doc shows 3
  • Missing: GET /:id, POST /bulk-permanent-delete, PATCH /bulk-delete, PATCH /bulk-restore, PATCH /:id, DELETE /:id/permanent, PATCH /:id/restore

D-10. 8 Public endpoints undocumented (and 2 documented ones don't exist) [HIGH]

  • Verified in: public.routes.ts — 8 actual routes, 0 match the 2 documented ones
  • Undocumented: GET /public/blogs, /public/menus, /public/menu-items, /public/home-page-blogs, /public/top-blogs, /public/blogs/featured-categories, /public/blogs/:categoryNameOrSlug/featured, /public/blogs/:id

D-11. Entire Coupon module undocumented [MEDIUM]

  • Verified in: index.ts line 77-79 mounts /coupons route from cupon.routes.ts. Entity: Cupon.ts (typo in filename)
  • Impact: Coupon functionality exists but is invisible to API consumers

D-12. 2 Payment endpoints undocumented + 6 Email Template endpoints undocumented [HIGH]

  • Payment (verified in payment.route.ts): GET /payments and POST /payments/stripe/customer-portal-session exist but are undocumented. Meanwhile the 2 documented payment endpoints don't exist.
  • Email Template (verified in email_template.routes.ts): Missing GET /all, PATCH /:id/restore, PATCH /bulk-delete, PATCH /bulk-restore, DELETE /:id/permanent

Category E: Security Issues (6 findings)

E-1. POST /roles permission check commented out — privilege escalation [CRITICAL]

  • Verified in: role.service.ts line 150: // await checkPermissionAndThrow("role.create", "create a role");
  • Impact: Any authenticated user can create arbitrary roles

E-2. LemonSqueezy webhook secret hardcoded [CRITICAL]

  • Verified in: webhook.controller.ts line 80: const secret = "c3b451346788c32" — config line commented out at line 79
  • Impact: Secret exposed in source code

E-3. GET /purchases has no permission check [HIGH]

  • Doc ref: api.md line 450: "Auth + Permission required"
  • Verified in: purchase.route.ts line 8: router.get("/", auth(), PurchaseController.getPurchaseList) — no hasPermission(), no contextMiddleware, no service-level check
  • Impact: Any authenticated user can list ALL purchases

E-4. Global rate limiting not applied [HIGH]

  • Doc ref: api.md line 921: "Default: 100 requests per 1 minute per IP (applied globally)"
  • Verified in: app.ts — no rateLimiter import or usage. Grep returned zero matches. Rate limiter only used on 3 specific auth routes.
  • Impact: No global rate limiting protection

E-5. Bearer token prefix not validated [MEDIUM]

  • Verified in: auth.ts — splits Authorization header on space, takes second element, never validates first is "Bearer". "Anything token123" would work.

E-6. verify-2fa-google leaks refreshToken in JSON body [MEDIUM]

  • Verified in: auth.contoroller.ts — unlike standard login (which strips refreshToken and sets it as cookie), this endpoint returns full LoginResponse including refreshToken in response body

Category F: Functional Inaccuracies (10 findings)

F-1. GET /menus/:slug is actually GET /menus/:id — numeric ID only, no slug [HIGH]

  • Doc ref: api.md line 624: "Get menu by slug with items."
  • Verified in: menu.routes.ts line 12: router.get("/:id", MenuController.getMenuById), menu.service.ts line 56: getMenuById = async (id: number) — forces numeric. No slug lookup exists.

F-2. POST /subscriptions path is wrong — actual: POST /subscriptions/subscribe [HIGH]

  • Doc ref: api.md lines 421-422
  • Verified in: subscription.routes.ts lines 13-18: router.post("/subscribe", ...)
  • Also: Doc says "Auth + Permission" but only auth() is enforced (no permission check). Undocumented requireStripe middleware. Body requires packageId, couponCode (optional), paymentGateway (required enum) — completely undocumented.

F-3. /auth/register rate limit undocumented [MEDIUM]

  • Verified in: auth.route.ts lines 32-38: windowMs: 1 * 60 * 1000, max: 3 (3 per minute)
  • Impact: Rate Limiting section omits this entirely

F-4. Login request body omits rememberMe field [MEDIUM]

  • Verified in: auth.validation.ts line 15: rememberMe: z.boolean().optional().default(false)
  • Impact: Feature undocumented

F-5. POST /auth/logout has no auth middleware [MEDIUM]

  • Doc ref: api.md line 207: "Auth required."
  • Verified in: auth.route.ts line 15: router.post("/logout", GoogleOAuthController.logout) — no auth() guard
  • Also: Response uses res.json() directly, not sendResponse wrapper

F-6. 4 of 5 LemonSqueezy webhook events are no-ops [MEDIUM]

  • Verified in: webhook.controller.ts lines 104-136: Only subscription_created has a real handler. Others are console.log only.
  • Also: LemonSqueezy webhook never sends a response (function ends without res.json() or res.send())

F-7. invoice.payment_succeeded Stripe handler commented out [MEDIUM]

  • Verified in: stripe.handler.ts line 27: handler call is commented. Switch case matches but does nothing.

F-8. Package durationInDays not a body field — auto-calculated [MEDIUM]

  • Doc ref: api.md line 384: shown in POST body
  • Verified in: package.validation.ts — not in schema. package.service.ts auto-calculates: trial=14, monthly=30, yearly=365

F-9. Required Package type field missing from doc [MEDIUM]

  • Verified in: package.validation.ts line 8: type: z.string() — required. Omitting causes 400 error.

F-10. PATCH /blogs/bulk-delete body expects blogIds, not ids [MEDIUM]

  • Doc ref: api.md lines 506-509: { "ids": [1, 2, 3] }
  • Verified in: blog.validation.ts lines 61-65: { blogIds: z.array(...) }

Category G: Database Schema vs Doc Mismatches (7 findings)

G-1. User has no refreshToken column [CRITICAL]

  • Verified in: src/entity/User.ts — no refreshToken field. Refresh tokens stored in separate Token entity with userId, token (hashed), expiresAt
  • Impact: api.md POST /auth/refresh-token implies user-level storage

G-2. permissions is NOT a User field [HIGH]

  • Doc ref: api.md login response shows permissions as direct user property
  • Verified in: User.ts — no permissions column. Resolved at runtime via Role -> ManyToMany -> Permission relation

G-3. Blog category relation is ManyToMany, not single categoryId [HIGH]

  • Doc ref: api.md line 481: "categoryId": 1
  • Verified in: Blog.ts@ManyToMany(() => BlogCategory) with blog_categories_pivot junction table. Old ManyToOne is commented out. categoryId exists as a plain column but actual relation is ManyToMany.

G-4. Blog tags are OneToMany (owned by blog), not standalone IDs [HIGH]

  • Doc ref: api.md line 482: "tags": [1, 2] implies standalone tag IDs
  • Verified in: Blog.ts@OneToMany(() => BlogTag, tag => tag.blog). Each BlogTag belongs to a single blog via blogId FK.

G-5. Subscription has no billingCycle column [MEDIUM]

  • Doc ref: api.md line 96: shows billingCycle inside subscription in login response
  • Verified in: Subscription.ts — only has status, startDate, endDate, stripeSubscriptionId. billingCycle lives on Package entity.

G-6. Setting entity has backupCodes column — likely copy-paste error [MEDIUM]

  • Verified in: Setting.ts — has backupCodes: simple-array column. Makes no logical sense on a settings table. UserSetting.ts also has backupCodes (correct location).

G-7. 17 entities have no response shape documented [LOW]

  • Payment, Purchase, Token, LoginAttempt, OtpVerification, UserRole, Coupon, BlogViewCount, GenericPage, AppConfig, BlogCategory, BlogTag, PackageCategory, Product, ProductCategory, Menu, Logger — all have zero entity shape documentation even when endpoints exist

Category H: Low-Severity / Code Quality (8 findings)

H-1. Permission route ordering bug [LOW]

  • Verified in: permission.routes.tsGET /:permissionId defined before GET /user-permissions, making the latter unreachable (Express matches "user-permissions" as a :permissionId param)

H-2. Controller filename typo: auth.contoroller.ts [LOW]

  • Verified in: File at src/app/modules/v1/Auth/auth.contoroller.ts

H-3. Password minimum length inconsistency [LOW]

  • Register validation: min(6), reset-password validation: min(3) (auth.validation.ts lines 7, 28)

H-4. Product category controller messages say "Blog" [LOW]

  • Verified in: product_category.controller.ts — 5 occurrences of "Blog category" in response messages

H-5. Product update controller returns 201/"created" instead of 200/"updated" [LOW]

  • Verified in: product.controller.ts lines 121-126

H-6. PATCH /:id/restore registered twice in Package routes [LOW]

  • Verified in: package.routes.ts lines 42-45 and 47-50 — identical duplicate

H-7. Total endpoints claim "90+" is a significant undercount [LOW]

  • Verified in: Route index has 30 mounted modules. Actual unique endpoint registrations: ~150+

H-8. Bare router.get expression in Purchase routes [LOW]

  • Verified in: purchase.route.ts line 9: router.get — standalone expression, does nothing

Summary

Category Count Severity Breakdown
A: Fictional endpoints 10 9 CRITICAL, 1 HIGH
B: Wrong auth levels 5 4 CRITICAL, 1 HIGH
C: Wrong shapes 17 11 HIGH, 3 MEDIUM
D: Missing endpoints 12 1 CRITICAL, 9 HIGH, 2 MEDIUM
E: Security issues 6 2 CRITICAL, 2 HIGH, 2 MEDIUM
F: Functional inaccuracies 10 2 HIGH, 8 MEDIUM
G: DB schema mismatches 7 1 CRITICAL, 3 HIGH, 2 MEDIUM, 1 LOW
H: Code quality / Low 8 8 LOW
Total 70 17 CRITICAL, 29 HIGH, 15 MEDIUM, 9 LOW

Round 2: Post-Rewrite Review (2026-02-25)

The api.md was completely rewritten to address all 70 Round 1 findings. This round reviews the rewrite itself. All 14 findings verified against actual source code.

R2-1. Login role is string, Profile role is object [MEDIUM]

  • Verified in: TokenHandler.ts createLoginResponse returns role: user.role.name (string). user.service.ts getProfile returns role: { id, name } (object).
  • Doc impact: api.md lines 122 vs 294 show contradictory shapes for the same field.

R2-2. Login response billingCycle comes from Package, not Subscription [LOW]

  • Verified in: TokenHandler.ts line 78: billingCycle: user?.subscription?.package?.billingCycle
  • Doc impact: api.md line 127 shows billingCycle inside subscription object without noting its source.

R2-3. Endpoint count "~160" is significantly wrong — actual: ~210 [MEDIUM]

  • Verified: Precise count of all route registrations (excluding InviteUser duplicate mount, Package duplicate /:id/restore, Swagger static): 210 unique routes.

R2-4. ~155 endpoints have no response shape documented [MEDIUM]

  • Only 6 endpoints show response shapes (login, profile, my-permissions, refresh-token, upload, analytics, checkout). The remaining endpoints show no response structure at all.

R2-5. GET /permissions/all described as "with pagination" — should note GET / is the unpaginated one [MEDIUM]

  • Verified in: permission.service.ts line 460: getAllPermissions uses getPaginationParams (paginated). Line 53: getPermissions returns all grouped permissions (NOT paginated).
  • Doc impact: The /all description is technically correct but the / description should explicitly say "returns all (not paginated)".

R2-6. Missing query parameters for most paginated list endpoints [MEDIUM]

  • Verified: Role, Permission, BlogTag, Contact, EmailTemplate, Menu, PackageCategory, Product, ProductCategory, BlogCategory, InviteUser, Coupon — all support pagination params but none document them.

R2-7. Missing permission strings for most endpoints [HIGH]

  • Verified: ~100+ checkPermissionAndThrow calls exist across all service files. The api.md only documents permission strings for ~10 endpoints. The remaining ~90+ are just "Permission checked in service" with no string.
  • Notable: Coupon module uses INCONSISTENT spelling — "cupon.view", "cupon.read", "cupon.delete" vs "coupon.create", "coupon.update", "coupon.view".

R2-8. POST /user-settings/2fa/generate-backup-codes code field is dead code [LOW]

  • Verified in: user_setting.routes.ts line 13 — no validateRequest() middleware. Controller never reads req.body. Service only takes userId.
  • Doc impact: api.md line 1435 documents a field that is never read.

R2-9. Dual lookup (ID or slug) lacks explicit examples [LOW]

  • Verified in: blog.service.ts line 313 — uses /^\d+$/ regex to distinguish.
  • Doc impact: Minor — behavior mentioned but not illustrated.

R2-10. "Permission checked via contextMiddleware" is misleading [MEDIUM]

  • Verified in: contextMiddleware.ts — populates context and warms permission cache. Never throws, never blocks, always calls next().
  • Doc impact: api.md line 1438 says "Permission checked via contextMiddleware" implying enforcement that does not exist.

R2-11. Product originalPrice is string type AND has no entity column [HIGH]

  • Verified in: product.validation.tsoriginalPrice: z.string().regex(...). Entity Product.ts has no originalPrice column; only discountPrice (decimal).
  • Doc impact: api.md documents originalPrice as a valid field but it may be silently dropped since the entity has no matching column.

R2-12. Blog status three-way inconsistency [MEDIUM]

  • Verified in: Entity type allows "draft"|"published"|"archived". Validation Zod enum: only "draft"|"published". Service query filter accepts "archived" and "all".
  • Doc impact: api.md only shows draft|published for create — correct for validation. Should note archived is query-only.

R2-13. verify-otp and verify-backup-code response shapes missing [HIGH]

  • Verified in: auth.contoroller.ts — verify-otp returns { user, token } but refreshToken is generated then discarded (not returned, not set as cookie). verify-backup-code returns { token, refreshToken, user } in body (refreshToken leaks, no cookie set).
  • Doc impact: Both are critical auth endpoints with no response shape documented.

R2-14. Coupon applyCoupon is dead code — controller + service exist but no route [LOW]

  • Verified in: cupon.routes.ts — no route references applyCoupon. The validation, controller, and service function all exist but are never wired.
  • Doc impact: None needed — not a route, so correctly excluded from api.md.

Round 2 Summary

Severity Count Findings
HIGH 3 R2-7, R2-11, R2-13
MEDIUM 7 R2-1, R2-3, R2-4, R2-5, R2-6, R2-10, R2-12
LOW 4 R2-2, R2-8, R2-9, R2-14
Total 14 ALL RESOLVED

Status: All 14 Round 2 findings addressed in api.md on 2026-02-25: - R2-1: Added note about role string vs object inconsistency - R2-2: Added note that billingCycle comes from Package relation - R2-3: Endpoint count corrected to ~210 - R2-4: Response shapes added for verify-otp and verify-backup-code (remaining endpoints acknowledged as gap) - R2-5: Fixed /permissions/all and /permissions/ descriptions - R2-6: Added query parameters to all paginated list endpoints - R2-7: Added ~100 permission strings to all endpoints with service-level checks - R2-8: Removed dead code field from generateBackupCodes - R2-9: Added ID/slug dual lookup examples - R2-10: Removed misleading contextMiddleware enforcement claim - R2-11: Added known-issue note about originalPrice string type and missing entity column - R2-12: Added blog status inconsistency note (archived query-only) - R2-13: Documented verify-otp and verify-backup-code response shapes with security notes - R2-14: No action needed (dead code, not a route)


Round 3: Post-Fix Review (2026-02-25)

Round 3 re-audited the api.md after Round 2 fixes. All findings verified against actual source code with file paths and line numbers.

R3-1. PATCH /users/profile accepts role — privilege escalation [CRITICAL]

  • Verified in: user.validation.tsupdateUserValidation is .partial() of createUserValidation which includes role: z.number(). user.service.ts line 533: user.roleId = payload.role || user.roleId — blindly applies.
  • Impact: Any authenticated user can change their own role to any role ID (including admin). Doc lists the field without security warning.

R3-2. Blog permission model completely undocumented [HIGH]

  • Verified in: blog.service.ts — ALL checkPermissionAndThrow calls are commented out (lines 51, 106, 352, 404, 475, 515, 552, 579, 610). Instead uses ownership-based hasPermission("global.blog-view"), hasPermission("global.blog-edit"), hasPermission("global.blog-delete"), hasPermission("global.blog-permanent-delete"), hasPermission("global.blog-restore"). Only active check: bulkBlogUpdateByCategory at line 260 uses checkPermissionAndThrow("blog.update").
  • Doc impact: Doc says "Auth required" with no permission strings. The global.blog-* strings and the hybrid model are invisible.

R3-3. DELETE /products/:id has no permission check [MEDIUM]

  • Verified in: product.service.ts deleteProductById — no checkPermissionAndThrow, no hasPermission, not even commented out. Any authenticated user can delete any product.

R3-4. GET /analytics refresh query param does not exist [MEDIUM]

  • Verified in: analytic.controller.ts line 10 — calls AnalyticService.getDashboardAnalytics() with no arguments. Does not read req.query. Service takes no params. getCache/addCache imports are dead code.
  • Doc impact: Doc fabricates a parameter that the code never reads.

R3-5. Page bulk-restore permission string wrong [MEDIUM]

  • Verified in: generic-page.service.ts line 286 — uses "generic-page.restore". Doc says page.restore.

R3-6. GET /subscriptions and /me missing 6+ query params [MEDIUM]

  • Verified in: subscription.service.ts line 53 — getSubscriptionList accepts page, limit, searchTerm, status (enum: active|expired|pending|cancelled), startDate, endDate, sort. Same for getMySubscriptions (line 152).

R3-7. MenuItem permission naming inconsistency not flagged [LOW]

  • Verified in: menuItem.service.ts — GET endpoints use menuItem.* (camelCase), CUD use menu-item.* (kebab-case). Doc lists both correctly but doesn't flag it as a known issue (unlike Coupon which gets a callout).

R3-8. Product update has commented-out permission with naming inconsistency [LOW]

  • Verified in: product.service.ts line 103 — // await checkPermissionAndThrow("product.update") (singular). Active create uses "products.create" (plural). Neither the commented-out check nor the inconsistency is documented.

R3-9. Coupon getAllActiveCoupons has undocumented permission call [LOW]

  • Verified in: cupon.service.ts line 356 — checkPermissionAndThrow("coupon.view") (correctly spelled, unlike getAllCupons which uses "cupon.view").

R3-10. Blog PATCH /blogs/bulk-update-category has active permission check undocumented [MEDIUM]

  • Verified in: blog.service.ts line 260 — checkPermissionAndThrow("blog.update") is active (not commented out). Doc shows no permission string for this endpoint.

Round 3 Summary

Severity Count Findings
CRITICAL 1 R3-1
HIGH 1 R3-2
MEDIUM 5 R3-3, R3-4, R3-5, R3-6, R3-10
LOW 3 R3-7, R3-8, R3-9
Total 10 ALL RESOLVED

Status: All 10 Round 3 findings addressed in api.md on 2026-02-25: - R3-1: Added SECURITY WARNING for PATCH /users/profile privilege escalation (role field) - R3-2: Documented entire blog hybrid permission model with global.blog-* strings - R3-3: Added known-issue note for DELETE /products/:id missing permission - R3-4: Removed fabricated refresh query param from GET /analytics - R3-5: Corrected page bulk-restore permission from page.restore to generic-page.restore - R3-6: Added 6+ query params to GET /subscriptions and GET /subscriptions/me - R3-7: Added known-issue note for MenuItem permission naming inconsistency - R3-8: Documented product update commented-out permission and naming inconsistency - R3-9: Acknowledged — minor (Coupon getAllActiveCoupons may be dead code or internal) - R3-10: Added Permission: blog.update to PATCH /blogs/bulk-update-category with note it's the only active check


Cumulative Totals

Round Findings Resolved
Round 1 70 70/70
Round 2 14 14/14
Round 3 10 10/10
Total 94 94/94

Round 1: adversarial code audit, 2026-02-25. Round 2: post-rewrite review, 2026-02-25. Round 3: post-fix review, 2026-02-25.