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 /andPOST /stripe/customer-portal-session - Actual checkout path:
POST /api/v1/subscriptions/subscribe(inSubscription/subscription.routes.tsline 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/routespath in themoduleRoutesarray. No Routes module exists anywhere undersrc/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— onlyGET /,GET /:prefix/prefix, andGET /:idexist. 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.tsline 9:router.post("/image", ...)mounted at/uploadsin 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.tsline 11:router.get("/:prefix/prefix", ...)—:prefixis a dynamic parameter, not literaladmin - Impact: Misleading, though calling
/settings/admin/prefixwould 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.tsline 12:router.get("/", ...)mounted at/analyticsin 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.tslines 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.tsline 12:router.use(auth(), contextMiddleware)before all routes includingGET /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.tsline 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.ts—router.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.tslines 10-13: all three GET routes registered BEFORErouter.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.tsline 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.tslines 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.tslines 70-74: field iscode, notbackupCode - 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.tslines 5-22: expects{ "role": "...", "displayName": "...", "permissionIds": [...] } - Impact: Three field name mismatches —
name->role,permissions->permissionIds,displayNamemissing
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 isrole(number), notroleId. 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.tslines 27-31: expects{ "userIds": [...] }, controller line 167 readsreq.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.tslines 4-10: field isname, notroute. Service mapspayload.nametoroutecolumn.
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.tslines 110-115: returns{ method: "otp_verification" }only — notokenoruserfields
C-9. Login 2FA Google method string wrong [MEDIUM]¶
- Doc ref: api.md line 108:
"google_authenticator" - Verified in:
auth.contoroller.tsline 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.tslines 26-35: expects{ "items": [{...}] }— array wrapped initemsproperty
C-11. Blog POST body — 6+ field mismatches [HIGH]¶
- Doc ref: api.md lines 476-488
- Verified in:
blog.validation.tslines 27-46 slugNOT in validation — not accepted on createcategoryIdshould becategories(array of numbers — ManyToMany relation)imageUrlis viaupload.single("image")file upload, not a JSON field- Content-Type is
multipart/form-data, notapplication/json authorNamerequired 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.tsline 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.tslines 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 inpermissionskey). 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 aresearchTerm(notsearch) andtrashOnly(notisDeleted)
C-16. Package query param billingCycle wrong — actual: billingType [MEDIUM]¶
- Doc ref: api.md line 370
- Verified in:
package.service.tsline 75: queries bybillingType
C-17. Paginated response meta missing totalPages [MEDIUM]¶
- Doc ref: api.md lines 35-41: shows
{ page, limit, total, totalPages } - Verified in:
sendResponse.tslines 7-10: type only haspage,limit,total— nototalPages
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 /andPOST / - 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/restoreregistered 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-usersAND/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.tsline 77-79 mounts/couponsroute fromcupon.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 /paymentsandPOST /payments/stripe/customer-portal-sessionexist but are undocumented. Meanwhile the 2 documented payment endpoints don't exist. - Email Template (verified in
email_template.routes.ts): MissingGET /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.tsline 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.tsline 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.tsline 8:router.get("/", auth(), PurchaseController.getPurchaseList)— nohasPermission(), nocontextMiddleware, 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— norateLimiterimport 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 fullLoginResponseincluding 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.tsline 12:router.get("/:id", MenuController.getMenuById),menu.service.tsline 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.tslines 13-18:router.post("/subscribe", ...) - Also: Doc says "Auth + Permission" but only
auth()is enforced (no permission check). UndocumentedrequireStripemiddleware. Body requirespackageId,couponCode(optional),paymentGateway(required enum) — completely undocumented.
F-3. /auth/register rate limit undocumented [MEDIUM]¶
- Verified in:
auth.route.tslines 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.tsline 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.tsline 15:router.post("/logout", GoogleOAuthController.logout)— noauth()guard - Also: Response uses
res.json()directly, notsendResponsewrapper
F-6. 4 of 5 LemonSqueezy webhook events are no-ops [MEDIUM]¶
- Verified in:
webhook.controller.tslines 104-136: Onlysubscription_createdhas a real handler. Others areconsole.logonly. - Also: LemonSqueezy webhook never sends a response (function ends without
res.json()orres.send())
F-7. invoice.payment_succeeded Stripe handler commented out [MEDIUM]¶
- Verified in:
stripe.handler.tsline 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.tsauto-calculates: trial=14, monthly=30, yearly=365
F-9. Required Package type field missing from doc [MEDIUM]¶
- Verified in:
package.validation.tsline 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.tslines 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 separateTokenentity withuserId,token(hashed),expiresAt - Impact: api.md
POST /auth/refresh-tokenimplies user-level storage
G-2. permissions is NOT a User field [HIGH]¶
- Doc ref: api.md login response shows
permissionsas 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)withblog_categories_pivotjunction table. Old ManyToOne is commented out.categoryIdexists 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 viablogIdFK.
G-5. Subscription has no billingCycle column [MEDIUM]¶
- Doc ref: api.md line 96: shows
billingCycleinside subscription in login response - Verified in:
Subscription.ts— only hasstatus,startDate,endDate,stripeSubscriptionId.billingCyclelives on Package entity.
G-6. Setting entity has backupCodes column — likely copy-paste error [MEDIUM]¶
- Verified in:
Setting.ts— hasbackupCodes: simple-arraycolumn. Makes no logical sense on a settings table.UserSetting.tsalso hasbackupCodes(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.ts—GET /:permissionIddefined beforeGET /user-permissions, making the latter unreachable (Express matches "user-permissions" as a:permissionIdparam)
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.tslines 121-126
H-6. PATCH /:id/restore registered twice in Package routes [LOW]¶
- Verified in:
package.routes.tslines 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.tsline 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.tscreateLoginResponsereturnsrole: user.role.name(string).user.service.tsgetProfilereturnsrole: { 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.tsline 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.tsline 460:getAllPermissionsusesgetPaginationParams(paginated). Line 53:getPermissionsreturns all grouped permissions (NOT paginated). - Doc impact: The
/alldescription 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+
checkPermissionAndThrowcalls 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.tsline 13 — novalidateRequest()middleware. Controller never readsreq.body. Service only takesuserId. - 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.tsline 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 callsnext(). - 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.ts—originalPrice: z.string().regex(...). EntityProduct.tshas nooriginalPricecolumn; onlydiscountPrice(decimal). - Doc impact: api.md documents
originalPriceas 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|publishedfor create — correct for validation. Should notearchivedis 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 referencesapplyCoupon. 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
codefield 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.ts—updateUserValidationis.partial()ofcreateUserValidationwhich includesrole: z.number().user.service.tsline 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— ALLcheckPermissionAndThrowcalls are commented out (lines 51, 106, 352, 404, 475, 515, 552, 579, 610). Instead uses ownership-basedhasPermission("global.blog-view"),hasPermission("global.blog-edit"),hasPermission("global.blog-delete"),hasPermission("global.blog-permanent-delete"),hasPermission("global.blog-restore"). Only active check:bulkBlogUpdateByCategoryat line 260 usescheckPermissionAndThrow("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.tsdeleteProductById— nocheckPermissionAndThrow, nohasPermission, 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.tsline 10 — callsAnalyticService.getDashboardAnalytics()with no arguments. Does not readreq.query. Service takes no params.getCache/addCacheimports 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.tsline 286 — uses"generic-page.restore". Doc sayspage.restore.
R3-6. GET /subscriptions and /me missing 6+ query params [MEDIUM]¶
- Verified in:
subscription.service.tsline 53 —getSubscriptionListacceptspage,limit,searchTerm,status(enum:active|expired|pending|cancelled),startDate,endDate, sort. Same forgetMySubscriptions(line 152).
R3-7. MenuItem permission naming inconsistency not flagged [LOW]¶
- Verified in:
menuItem.service.ts— GET endpoints usemenuItem.*(camelCase), CUD usemenu-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.tsline 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.tsline 356 —checkPermissionAndThrow("coupon.view")(correctly spelled, unlikegetAllCuponswhich uses"cupon.view").
R3-10. Blog PATCH /blogs/bulk-update-category has active permission check undocumented [MEDIUM]¶
- Verified in:
blog.service.tsline 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 fabricatedrefreshquery param from GET /analytics - R3-5: Corrected page bulk-restore permission frompage.restoretogeneric-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 (CoupongetAllActiveCouponsmay be dead code or internal) - R3-10: Added Permission:blog.updateto 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.