feat: add contact sales leads endpoints (SITES-42069)#2053
feat: add contact sales leads endpoints (SITES-42069)#2053tarunsinghdev merged 9 commits intomainfrom
Conversation
|
This PR will trigger a minor release when merged. |
There was a problem hiding this comment.
Code Review
Critical Issues
1. No access control checks
None of the endpoints use AccessControlUtil. Per project conventions, every endpoint returning tenant-specific data must check access control:
getByOrganizationId— returns all leads for an org with no access check. Any authenticated IMS user can read any org's leads.checkBySite— same issue, leaks existence of leads across tenants.updateStatus— any authenticated user can update any lead's status.create— resolves org from IMS token but doesn't verify the user has access to that org.
This is a security issue for a table containing PII (name, email).
2. create fetches ALL leads for an org to check duplicates
const leads = await ContactSalesLead.allByOrganizationId(organizationId);
const duplicateForSite = leads.find((lead) => lead.getSiteId() === siteId);This loads every lead for the org into memory just to find one duplicate. For orgs with many leads, this is an unbounded query. Use a targeted query like findByOrganizationIdAndSiteId() or rely on the DB unique constraint and catch the conflict error.
3. checkBySite has the same full-scan problem
const leads = await ContactSalesLead.allByOrganizationId(organizationId);
const match = leads.find((lead) => lead.getSiteId() === siteId);Fetches all leads for an org, filters in JS. This endpoint is called on every page load (per PR description: "Used by the frontend to determine button state"). Should be a direct DB lookup.
4. internalServerError(e.message) leaks internal error details
return internalServerError(e.message);This appears in every catch block. Raw DB error messages (constraint violations, connection errors) will be returned to the client. Use cleanupHeaderValue() or return a generic message per project conventions.
Other Issues
5. No OpenAPI spec
The PR checklist calls out updating the API spec, but no docs/openapi/ changes are included. Per project conventions: "OpenAPI First — Define API contract in docs/openapi/ before implementation."
6. getByOrganizationId returns 400 for missing org — should be 404
if (!organization) {
return badRequest('Organization not found');
}A valid UUID that doesn't match an existing org is a "not found" condition, not a bad request. Should be notFound().
7. PR title doesn't follow conventional commits
Current: Feature/sites 42069 contact sales lead
Should be: feat: add contact sales leads endpoints (SITES-42069)
8. resolveOrganizationId silently swallows errors
try {
const org = await Organization.findByImsOrgId(imsOrgId);
return org ? org.getId() : null;
} catch {
return null;
}If the DB is down, this silently returns null, causing create to fall through to the global findByEmail path instead of failing. At minimum, log the error.
**9. updateStatus is misleading — should be a generic update
The PATCH /contact-sales-leads/:contactSalesLeadId endpoint is named updateStatus but it's the only PATCH route for this resource. If more fields become updatable later (e.g., notes, domain), this name forces either renaming or adding a second PATCH endpoint. Follow the convention used by other controllers — name it update and accept a partial body. The route is already PATCH, which implies partial update semantics.
|
Fixes for Code Review Addressed all actionable items from @sandsinh's review: Critical Issues — Fixed
Other Issues — Fixed
Additional Changes
|
| 'DELETE /sites/:siteId/ims-org-access/:accessId': imsOrgAccessController.revokeGrant, | ||
|
|
||
| // Contact Sales Leads | ||
| 'POST /contact-sales-leads': contactSalesLeadsController.create, |
There was a problem hiding this comment.
We can also make this per site ?
POST /organizations/:organizationId/sites/:siteId/contact-sales-lead
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
# [1.407.0](v1.406.0...v1.407.0) (2026-04-02) ### Features * add contact sales leads endpoints (SITES-42069) ([#2053](#2053)) ([915702e](915702e))
|
🎉 This PR is included in version 1.407.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
[SITES-42069](https://jira.corp.adobe.com/browse/SITES-42069) ### `spacecat-api-service` — API Endpoints **New controller: `ContactSalesLeadsController`** | File | Description | |------|-------------| | `src/controllers/contact-sales-leads.js` | Controller with `create`, `getByOrganizationId`, `checkBySite`, and `updateStatus` methods | | `src/dto/contact-sales-lead.js` | DTO serializer (`toJSON`) for API responses — includes `siteId` | | `src/routes/index.js` | Route registration (merged with upstream changes) | | `src/index.js` | Controller instantiation (merged with upstream changes) | | `test/controllers/contact-sales-leads.test.js` | Full unit test suite (mocha + chai + sinon + esmock) | **Endpoints:** | Method | Route | Auth | Description | |--------|-------|------|-------------| | `POST` | `/contact-sales-leads` | IMS (any authenticated user) | Creates a new lead; returns `201` on success, `409` if a duplicate exists for the same org+site (when `siteId` is sent) or same org+email with no site (when `siteId` is omitted) | | `GET` | `/organizations/:organizationId/contact-sales-leads` | IMS (any authenticated user) | Lists all leads for an organization | | `GET` | `/organizations/:organizationId/sites/:siteId/contact-sales-lead` | IMS (any authenticated user) | Checks if a lead exists for a given org+site; returns `{ exists: true/false }` | | `PATCH` | `/contact-sales-leads/:contactSalesLeadId` | IMS (any authenticated user) | Updates a lead's status (`NEW` → `CONTACTED` → `CLOSED`) | **`POST /contact-sales-leads` behavior:** - Validates `name` (required, non-empty) and `email` (required, valid format) - Accepts optional `domain` (the site baseURL) and optional `siteId` (validated as UUID) - Checks for duplicates in scope of the resolved organization: if `siteId` is present, `409` when a lead already exists for that org+site; if `siteId` is omitted, `409` when a lead already exists for that org with the same email and no `siteId`. If the org cannot be resolved, falls back to a global email check via `findByEmail` - Resolves the internal `organizationId` from the authenticated user's IMS org - Creates the lead with status `NEW` - Returns the serialized lead DTO with `201 Created` **`GET /organizations/:organizationId/sites/:siteId/contact-sales-lead` behavior (new):** - Validates both `organizationId` and `siteId` are valid UUIDs - Fetches all leads for the organization, then finds one matching the given `siteId` - Returns `{ exists: true, lead: <DTO> }` if found, `{ exists: false }` otherwise - Used by the frontend to determine button state on page load **`GET /organizations/:organizationId/contact-sales-leads` behavior:** - Validates `organizationId` is a valid UUID - Checks organization exists - Returns array of serialized lead DTOs **`PATCH /contact-sales-leads/:contactSalesLeadId` behavior:** - Validates `contactSalesLeadId` is a valid UUID - Validates `status` is one of: `NEW`, `CONTACTED`, `CLOSED` - Returns `404` if lead not found - Calls `lead.setStatus(status)` + `lead.save()`, returns updated DTO with `200 OK` **Test coverage (21 test cases):** | Endpoint | Tests | Description | |----------|-------|-------------| | `create` | 8 | Successful creation with org resolution, creation with siteId, creation without siteId, 400 for missing name / missing email / invalid email, 409 for duplicate org+site, 409 for duplicate org+email (no site), 500 for unexpected DB error | | `getByOrganizationId` | 3 | Successful listing, 400 for invalid org ID, 400 for missing org | | `checkBySite` | 5 | Returns `exists: true` when lead matches org+site, returns `exists: false` for no match, 400 for invalid org ID, 400 for invalid site ID, 500 for DB error | | `updateStatus` | 5 | Successful status update, 400 for invalid lead ID, 400 for invalid status value, 404 for lead not found, 500 for unexpected DB error | --- Please ensure your pull request adheres to the following guidelines: - [x] make sure to link the related issues in this description. Or if there's no issue created, make sure you describe here the problem you're solving. - [ ] when merging / squashing, make sure the fixed issue references are visible in the commits, for easy compilation of release notes If the PR is changing the API specification: - [ ] make sure you add a "Not implemented yet" note the endpoint description, if the implementation is not ready yet. Ideally, return a 501 status code with a message explaining the feature is not implemented yet. - [ ] make sure you add at least one example of the request and response. If the PR is changing the API implementation or an entity exposed through the API: - [ ] make sure you update the API specification and the examples to reflect the changes. If the PR is introducing a new audit type: - [ ] make sure you update the API specification with the type, schema of the audit result and an example ## Related Issues Thanks for contributing!
# [1.407.0](v1.406.0...v1.407.0) (2026-04-02) ### Features * add contact sales leads endpoints (SITES-42069) ([#2053](#2053)) ([915702e](915702e))
SITES-42069
spacecat-api-service— API EndpointsNew controller:
ContactSalesLeadsControllersrc/controllers/contact-sales-leads.jscreate,getByOrganizationId,checkBySite, andupdateStatusmethodssrc/dto/contact-sales-lead.jstoJSON) for API responses — includessiteIdsrc/routes/index.jssrc/index.jstest/controllers/contact-sales-leads.test.jsEndpoints:
POST/contact-sales-leads201on success,409if a duplicate exists for the same org+site (whensiteIdis sent) or same org+email with no site (whensiteIdis omitted)GET/organizations/:organizationId/contact-sales-leadsGET/organizations/:organizationId/sites/:siteId/contact-sales-lead{ exists: true/false }PATCH/contact-sales-leads/:contactSalesLeadIdNEW→CONTACTED→CLOSED)POST /contact-sales-leadsbehavior:name(required, non-empty) andemail(required, valid format)domain(the site baseURL) and optionalsiteId(validated as UUID)siteIdis present,409when a lead already exists for that org+site; ifsiteIdis omitted,409when a lead already exists for that org with the same email and nositeId. If the org cannot be resolved, falls back to a global email check viafindByEmailorganizationIdfrom the authenticated user's IMS orgNEW201 CreatedGET /organizations/:organizationId/sites/:siteId/contact-sales-leadbehavior (new):organizationIdandsiteIdare valid UUIDssiteId{ exists: true, lead: <DTO> }if found,{ exists: false }otherwiseGET /organizations/:organizationId/contact-sales-leadsbehavior:organizationIdis a valid UUIDPATCH /contact-sales-leads/:contactSalesLeadIdbehavior:contactSalesLeadIdis a valid UUIDstatusis one of:NEW,CONTACTED,CLOSED404if lead not foundlead.setStatus(status)+lead.save(), returns updated DTO with200 OKTest coverage (21 test cases):
creategetByOrganizationIdcheckBySiteexists: truewhen lead matches org+site, returnsexists: falsefor no match, 400 for invalid org ID, 400 for invalid site ID, 500 for DB errorupdateStatusPlease ensure your pull request adheres to the following guidelines:
describe here the problem you're solving.
If the PR is changing the API specification:
yet. Ideally, return a 501 status code with a message explaining the feature is not implemented yet.
If the PR is changing the API implementation or an entity exposed through the API:
If the PR is introducing a new audit type:
Related Issues
Thanks for contributing!