From 0e5c7dc05b77f78a4c7c79ddeb4862dd443a0093 Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Wed, 8 Jul 2026 00:18:36 +0530 Subject: [PATCH 1/8] feat: expose organization domains in public APIs Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com> --- backend/src/api/public/openapi.yaml | 47 +++++++++-- .../createMemberWorkExperience.ts | 2 +- .../getMemberWorkExperiences.ts | 2 +- .../updateMemberWorkExperience.ts | 2 +- .../verifyMemberWorkExperience.ts | 10 ++- .../v1/organizations/createOrganization.ts | 2 +- .../v1/organizations/getOrganization.ts | 54 ++++--------- backend/src/utils/mapper.ts | 1 + .../src/members/organizations.ts | 63 +++++++++++---- .../src/organizations/base.ts | 78 +++++++++++++++++++ services/libs/types/src/organizations.ts | 1 + 11 files changed, 195 insertions(+), 67 deletions(-) diff --git a/backend/src/api/public/openapi.yaml b/backend/src/api/public/openapi.yaml index 0a3089ca9a..7af24dfac0 100644 --- a/backend/src/api/public/openapi.yaml +++ b/backend/src/api/public/openapi.yaml @@ -779,8 +779,11 @@ paths: /organizations: get: operationId: getOrganization - summary: Look up an organization by domain - description: Find a verified organization by its primary domain. + summary: Look up an organization by domain or name + description: > + Provide domain, name, or both. When both are provided, the domain and + name must belong to the same organization. If multiple organizations + match, the most active one is returned. tags: - Organizations security: @@ -789,12 +792,20 @@ paths: parameters: - name: domain in: query - required: true + required: false description: Primary domain of the organization. schema: type: string minLength: 1 example: linuxfoundation.org + - name: name + in: query + required: false + description: Exact display name of the organization. + schema: + type: string + minLength: 1 + example: Linux Foundation responses: '200': description: Organization found. @@ -804,14 +815,15 @@ paths: $ref: '#/components/schemas/Organization' example: id: 550e8400-e29b-41d4-a716-446655440000 - name: Linux Foundation + displayName: Linux Foundation + domain: linuxfoundation.org logo: https://example.com/logo.png '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': - description: No verified organization found for the given domain. + description: No organization found for the given domain or name. content: application/json: schema: @@ -876,15 +888,26 @@ paths: required: - id - name + - domain properties: id: type: string format: uuid name: type: string + domain: + type: string + description: Verified primary domain of the organization. + logo: + type: + - string + - 'null' + description: URL of the organization logo. example: id: 550e8400-e29b-41d4-a716-446655440000 name: Acme Corp + domain: acme.com + logo: https://example.com/logo.png '400': $ref: '#/components/responses/BadRequest' '401': @@ -1218,6 +1241,7 @@ components: - id - organizationId - organizationName + - organizationDomains - jobTitle - verified - verifiedBy @@ -1243,6 +1267,11 @@ components: - string - 'null' description: URL of the organization logo. + organizationDomains: + type: array + items: + type: string + description: Verified primary domains for the organization, in alphabetical order. jobTitle: type: - string @@ -1530,14 +1559,18 @@ components: type: object required: - id - - name + - displayName + - domain properties: id: type: string format: uuid - name: + displayName: type: string description: Display name of the organization. + domain: + type: string + description: Verified primary domain. logo: type: string description: URL of the organization logo. Only present if available. diff --git a/backend/src/api/public/v1/members/work-experiences/createMemberWorkExperience.ts b/backend/src/api/public/v1/members/work-experiences/createMemberWorkExperience.ts index 86c8d7e8c7..f84e7d259c 100644 --- a/backend/src/api/public/v1/members/work-experiences/createMemberWorkExperience.ts +++ b/backend/src/api/public/v1/members/work-experiences/createMemberWorkExperience.ts @@ -117,7 +117,7 @@ export async function createMemberWorkExperience(req: Request, res: Response): P memberOrganizationIds: [data.organizationId], }) - const orgsMap = await fetchManyMemberOrgsWithOrgData(qx, [memberId]) + const orgsMap = await fetchManyMemberOrgsWithOrgData(qx, [memberId], { withDomains: true }) createdMo = (orgsMap.get(memberId) ?? []).find((mo) => mo.id === newMemberOrgId) captureNewState(createdMo ?? null) diff --git a/backend/src/api/public/v1/members/work-experiences/getMemberWorkExperiences.ts b/backend/src/api/public/v1/members/work-experiences/getMemberWorkExperiences.ts index c253f49f3b..ed000b68a4 100644 --- a/backend/src/api/public/v1/members/work-experiences/getMemberWorkExperiences.ts +++ b/backend/src/api/public/v1/members/work-experiences/getMemberWorkExperiences.ts @@ -27,7 +27,7 @@ export async function getMemberWorkExperiences(req: Request, res: Response): Pro throw new NotFoundError('Member not found') } - const orgsMap = await fetchManyMemberOrgsWithOrgData(qx, [memberId]) + const orgsMap = await fetchManyMemberOrgsWithOrgData(qx, [memberId], { withDomains: true }) const workExperiences = groupMemberOrganizations(orgsMap.get(memberId) ?? []).map( toMemberWorkExperience, ) diff --git a/backend/src/api/public/v1/members/work-experiences/updateMemberWorkExperience.ts b/backend/src/api/public/v1/members/work-experiences/updateMemberWorkExperience.ts index 444fc75cad..62ce2277f0 100644 --- a/backend/src/api/public/v1/members/work-experiences/updateMemberWorkExperience.ts +++ b/backend/src/api/public/v1/members/work-experiences/updateMemberWorkExperience.ts @@ -120,7 +120,7 @@ export async function updateMemberWorkExperience(req: Request, res: Response): P memberOrganizationIds: [data.organizationId], }) - const orgsMap = await fetchManyMemberOrgsWithOrgData(qx, [memberId]) + const orgsMap = await fetchManyMemberOrgsWithOrgData(qx, [memberId], { withDomains: true }) const updatedMo = groupMemberOrganizations(orgsMap.get(memberId) ?? []).find( (mo) => mo.id === workExperienceId, diff --git a/backend/src/api/public/v1/members/work-experiences/verifyMemberWorkExperience.ts b/backend/src/api/public/v1/members/work-experiences/verifyMemberWorkExperience.ts index f55a68a7c1..a43b640fc7 100644 --- a/backend/src/api/public/v1/members/work-experiences/verifyMemberWorkExperience.ts +++ b/backend/src/api/public/v1/members/work-experiences/verifyMemberWorkExperience.ts @@ -100,13 +100,19 @@ export async function verifyMemberWorkExperience(req: Request, res: Response): P }), ) - const orgsMap = await fetchManyMemberOrgsWithOrgData(qx, [memberId]) + const orgsMap = await fetchManyMemberOrgsWithOrgData(qx, [memberId], { withDomains: true }) const responseMo: IMemberRoleWithOrganization = groupMemberOrganizations(orgsMap.get(memberId) ?? []).find( (mo) => mo.id === workExperienceId, ) ?? - ({ ...memberOrg, ...updatedMemberOrg, verified, verifiedBy } as IMemberRoleWithOrganization) + ({ + ...memberOrg, + ...updatedMemberOrg, + verified, + verifiedBy, + organizationDomains: [], + } as IMemberRoleWithOrganization) ok(res, toMemberWorkExperience(responseMo)) } diff --git a/backend/src/api/public/v1/organizations/createOrganization.ts b/backend/src/api/public/v1/organizations/createOrganization.ts index d8f47714a1..199b898983 100644 --- a/backend/src/api/public/v1/organizations/createOrganization.ts +++ b/backend/src/api/public/v1/organizations/createOrganization.ts @@ -61,5 +61,5 @@ export async function createOrganization(req: Request, res: Response): Promise data.name || data.domain, { + message: 'Either name or domain must be provided', + }) export async function getOrganization(req: Request, res: Response): Promise { - const { domain } = validateOrThrow(querySchema, req.query) - + const { name, domain } = validateOrThrow(querySchema, req.query) const qx = optionsQx(req) - const results = await queryOrgIdentities(qx, { - fields: [OrgIdentityField.ORGANIZATION_ID], - filter: { - and: [ - { value: { eq: normalizeHostname(domain, false) } }, - { type: { eq: OrganizationIdentityType.PRIMARY_DOMAIN } }, - { verified: { eq: true } }, - ], - }, + const organization = await findOrganizationByNameOrDomain(qx, { + name, + domain: domain ? normalizeHostname(domain, false) : undefined, }) - const organizationId = results[0]?.organizationId - - if (!organizationId) { + if (!organization) { throw new NotFoundError('Organization not found') } - const org = await findOrgById(qx, organizationId, [ - OrganizationField.ID, - OrganizationField.DISPLAY_NAME, - ]) - - const attributes = await findOrgAttributes(qx, organizationId) - const logo = attributes.find((a) => a.name === 'logo')?.value - - ok(res, { - id: org.id, - name: org.displayName, - ...(logo ? { logo } : {}), - }) + ok(res, organization) } diff --git a/backend/src/utils/mapper.ts b/backend/src/utils/mapper.ts index 02e072f4e2..d4f7e57c9a 100644 --- a/backend/src/utils/mapper.ts +++ b/backend/src/utils/mapper.ts @@ -182,6 +182,7 @@ export function toMemberWorkExperience(mo: IMemberRoleWithOrganization) { organizationId: mo.organizationId, organizationName: mo.organizationName, organizationLogo: mo.organizationLogo, + organizationDomains: mo.organizationDomains ?? [], jobTitle: mo.title ?? null, verified: mo.verified ?? false, verifiedBy: mo.verifiedBy ?? null, diff --git a/services/libs/data-access-layer/src/members/organizations.ts b/services/libs/data-access-layer/src/members/organizations.ts index debe4b21dc..2d2e9be5e6 100644 --- a/services/libs/data-access-layer/src/members/organizations.ts +++ b/services/libs/data-access-layer/src/members/organizations.ts @@ -213,27 +213,60 @@ export async function fetchManyMemberOrgs( export async function fetchManyMemberOrgsWithOrgData( qx: QueryExecutor, memberIds: string[], + { withDomains = false }: { withDomains?: boolean } = {}, ): Promise> { - const memberRoles = (await qx.select( - ` - SELECT mo.*, o."displayName" as "organizationName", o.logo as "organizationLogo" - FROM "memberOrganizations" mo - join "organizations" o on mo."organizationId" = o.id - WHERE mo."memberId" in ($(memberIds:csv)) + const domainSelect = withDomains + ? `, + COALESCE(oid.domains, '{}') AS "organizationDomains"` + : '' + + const domainJoin = withDomains + ? ` + LEFT JOIN ( + SELECT + oi."organizationId", + array_agg(DISTINCT lower(oi.value) ORDER BY lower(oi.value)) AS domains + FROM "organizationIdentities" oi + WHERE oi.type = 'primary-domain' + AND oi.verified = true + AND oi."organizationId" IN ( + SELECT DISTINCT mo2."organizationId" + FROM "memberOrganizations" mo2 + WHERE mo2."memberId" IN ($(memberIds:csv)) + AND mo2."deletedAt" IS NULL + ) + GROUP BY oi."organizationId" + ) oid ON oid."organizationId" = mo."organizationId"` + : '' + + const sql = ` + SELECT + mo.*, + o."displayName" AS "organizationName", + o.logo AS "organizationLogo" + ${domainSelect} + FROM "memberOrganizations" mo + JOIN organizations o ON o.id = mo."organizationId" + ${domainJoin} + WHERE mo."memberId" IN ($(memberIds:csv)) AND mo."deletedAt" IS NULL; - `, - { - memberIds, - }, - )) as IMemberRoleWithOrganization[] + ` + + const memberRoles = (await qx.select(sql, { + memberIds, + })) as IMemberRoleWithOrganization[] + + const result = new Map() - const resultMap = new Map() for (const memberId of memberIds) { - const roles = memberRoles.filter((r) => r.memberId === memberId) - resultMap.set(memberId, roles) + result.set(memberId, []) } - return resultMap + for (const role of memberRoles) { + result.get(role.memberId)!.push(role) + } + + return result } export async function fetchManyOrganizationAffiliationPolicies( diff --git a/services/libs/data-access-layer/src/organizations/base.ts b/services/libs/data-access-layer/src/organizations/base.ts index a638938364..53c1990e68 100644 --- a/services/libs/data-access-layer/src/organizations/base.ts +++ b/services/libs/data-access-layer/src/organizations/base.ts @@ -728,3 +728,81 @@ export async function findNonExistingOrganizationIds( return rows.map((r: { id: string }) => r.id) } + +type OrganizationSummary = Pick & { + domain: string +} + +export async function findOrganizationByNameOrDomain( + qx: QueryExecutor, + { name, domain }: { name?: string; domain?: string }, +): Promise { + if (!name && !domain) { + return null + } + + const domainJoin = domain + ? ` + INNER JOIN "organizationIdentities" oi + ON oi."organizationId" = o.id + AND oi.type = 'primary-domain' + AND oi.verified = true + AND lower(oi.value) = lower($(domain))` + : '' + + const filters = ['o."deletedAt" IS NULL'] + + if (name) { + filters.push( + `trim(lower(o."displayName")) = trim(lower($(name)))`, + `EXISTS ( + SELECT 1 + FROM "organizationIdentities" oi_check + WHERE oi_check."organizationId" = o.id + AND oi_check.type = 'primary-domain' + AND oi_check.verified = true + )`, + ) + } + + const domainSelect = domain + ? 'lower($(domain)) AS domain' + : `( + SELECT lower(oi_domain.value) + FROM "organizationIdentities" oi_domain + WHERE oi_domain."organizationId" = o.id + AND oi_domain.type = 'primary-domain' + AND oi_domain.verified = true + ORDER BY lower(oi_domain.value) + LIMIT 1 + ) AS domain` + + const sql = ` + SELECT + o.id, + o."displayName", + o.logo, + ${domainSelect} + FROM "organizations" o + ${domainJoin} + LEFT JOIN "organizationsGlobalActivityCount" gac + ON gac."organizationId" = o.id + WHERE + ${filters.join(' AND ')} + ORDER BY + COALESCE(gac.total_count_estimate, 0) DESC, + ( + SELECT COUNT(DISTINCT mo."memberId") + FROM "memberOrganizations" mo + WHERE mo."organizationId" = o.id + AND mo."deletedAt" IS NULL + ) DESC, + o.id + LIMIT 1; + ` + + return qx.selectOneOrNone(sql, { + name, + domain, + }) +} diff --git a/services/libs/types/src/organizations.ts b/services/libs/types/src/organizations.ts index 40769253a6..85674161f8 100644 --- a/services/libs/types/src/organizations.ts +++ b/services/libs/types/src/organizations.ts @@ -94,6 +94,7 @@ export interface IRenderFriendlyMemberOrganization { export interface IMemberRoleWithOrganization extends IMemberOrganization { organizationName: string organizationLogo: string + organizationDomains?: string[] } export interface MemberOrgDate { From 69a62c36cfe9e3fefc3a7db3e952c70e968368ad Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Wed, 8 Jul 2026 00:31:10 +0530 Subject: [PATCH 2/8] fix: resolve pr review comments Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com> --- .../verifyMemberWorkExperience.ts | 31 ++++++++++++------- .../src/members/organizations.ts | 5 ++- .../src/organizations/base.ts | 2 +- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/backend/src/api/public/v1/members/work-experiences/verifyMemberWorkExperience.ts b/backend/src/api/public/v1/members/work-experiences/verifyMemberWorkExperience.ts index a43b640fc7..650e3ad92c 100644 --- a/backend/src/api/public/v1/members/work-experiences/verifyMemberWorkExperience.ts +++ b/backend/src/api/public/v1/members/work-experiences/verifyMemberWorkExperience.ts @@ -52,6 +52,18 @@ export async function verifyMemberWorkExperience(req: Request, res: Response): P throw new NotFoundError('Work experience not found') } + const orgsMapBeforeChange = await fetchManyMemberOrgsWithOrgData(qx, [memberId], { + withDomains: true, + }) + + const workExperienceWithOrgData = groupMemberOrganizations( + orgsMapBeforeChange.get(memberId) ?? [], + ).find((mo) => mo.id === workExperienceId) + + if (!workExperienceWithOrgData) { + throw new NotFoundError('Work experience not found') + } + const overlappingGroupedRows = getOverlappingGroupedMemberOrganizations(memberOrgs, memberOrg) const memberOrgIdsToDelete = [ @@ -102,17 +114,14 @@ export async function verifyMemberWorkExperience(req: Request, res: Response): P const orgsMap = await fetchManyMemberOrgsWithOrgData(qx, [memberId], { withDomains: true }) - const responseMo: IMemberRoleWithOrganization = - groupMemberOrganizations(orgsMap.get(memberId) ?? []).find( - (mo) => mo.id === workExperienceId, - ) ?? - ({ - ...memberOrg, - ...updatedMemberOrg, - verified, - verifiedBy, - organizationDomains: [], - } as IMemberRoleWithOrganization) + const responseMo: IMemberRoleWithOrganization = groupMemberOrganizations( + orgsMap.get(memberId) ?? [], + ).find((mo) => mo.id === workExperienceId) ?? { + ...workExperienceWithOrgData, + ...updatedMemberOrg, + verified, + verifiedBy, + } ok(res, toMemberWorkExperience(responseMo)) } diff --git a/services/libs/data-access-layer/src/members/organizations.ts b/services/libs/data-access-layer/src/members/organizations.ts index 2d2e9be5e6..a4d6c5a3fb 100644 --- a/services/libs/data-access-layer/src/members/organizations.ts +++ b/services/libs/data-access-layer/src/members/organizations.ts @@ -263,7 +263,10 @@ export async function fetchManyMemberOrgsWithOrgData( } for (const role of memberRoles) { - result.get(role.memberId)!.push(role) + const roles = result.get(role.memberId) + if (roles) { + roles.push(role) + } } return result diff --git a/services/libs/data-access-layer/src/organizations/base.ts b/services/libs/data-access-layer/src/organizations/base.ts index 53c1990e68..a6f4e9b4fa 100644 --- a/services/libs/data-access-layer/src/organizations/base.ts +++ b/services/libs/data-access-layer/src/organizations/base.ts @@ -766,7 +766,7 @@ export async function findOrganizationByNameOrDomain( } const domainSelect = domain - ? 'lower($(domain)) AS domain' + ? 'lower(oi.value) AS domain' : `( SELECT lower(oi_domain.value) FROM "organizationIdentities" oi_domain From 9d823caf64cf6939e4879d7d5bfbe99b86fd3d26 Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Wed, 8 Jul 2026 00:35:13 +0530 Subject: [PATCH 3/8] fix: resolve pr review comments Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com> --- backend/src/api/public/openapi.yaml | 6 +++--- backend/src/api/public/v1/organizations/getOrganization.ts | 3 ++- services/libs/data-access-layer/src/organizations/base.ts | 5 +++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/backend/src/api/public/openapi.yaml b/backend/src/api/public/openapi.yaml index 7af24dfac0..3be905cd7c 100644 --- a/backend/src/api/public/openapi.yaml +++ b/backend/src/api/public/openapi.yaml @@ -815,7 +815,7 @@ paths: $ref: '#/components/schemas/Organization' example: id: 550e8400-e29b-41d4-a716-446655440000 - displayName: Linux Foundation + name: Linux Foundation domain: linuxfoundation.org logo: https://example.com/logo.png '401': @@ -1559,13 +1559,13 @@ components: type: object required: - id - - displayName + - name - domain properties: id: type: string format: uuid - displayName: + name: type: string description: Display name of the organization. domain: diff --git a/backend/src/api/public/v1/organizations/getOrganization.ts b/backend/src/api/public/v1/organizations/getOrganization.ts index bbdf6abf5d..7316770d7f 100644 --- a/backend/src/api/public/v1/organizations/getOrganization.ts +++ b/backend/src/api/public/v1/organizations/getOrganization.ts @@ -29,5 +29,6 @@ export async function getOrganization(req: Request, res: Response): Promise r.id) } -type OrganizationSummary = Pick & { +type OrganizationSummary = Pick & { + name: string domain: string } @@ -780,7 +781,7 @@ export async function findOrganizationByNameOrDomain( const sql = ` SELECT o.id, - o."displayName", + o."displayName" AS name, o.logo, ${domainSelect} FROM "organizations" o From e109e0d0920246a3be0fa594db6df01e3e40e5c3 Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:12:14 +0530 Subject: [PATCH 4/8] fix: validate and normalize organization domain in creation process Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com> --- .../v1/organizations/createOrganization.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/backend/src/api/public/v1/organizations/createOrganization.ts b/backend/src/api/public/v1/organizations/createOrganization.ts index 199b898983..2ae7b3abae 100644 --- a/backend/src/api/public/v1/organizations/createOrganization.ts +++ b/backend/src/api/public/v1/organizations/createOrganization.ts @@ -2,7 +2,7 @@ import type { Request, Response } from 'express' import { z } from 'zod' import { captureApiChange, organizationCreateAction } from '@crowd/audit-logs' -import { InternalError } from '@crowd/common' +import { BadRequestError, InternalError, normalizeHostname } from '@crowd/common' import { findOrCreateOrganization, optionsQx } from '@crowd/data-access-layer' import { OrganizationAttributeSource, OrganizationIdentityType } from '@crowd/types' @@ -17,16 +17,20 @@ const bodySchema = z.object({ }) export async function createOrganization(req: Request, res: Response): Promise { - const { name, domain, source, logo } = validateOrThrow(bodySchema, req.body) + const { name, domain: rawDomain, source, logo } = validateOrThrow(bodySchema, req.body) - const qx = optionsQx(req) + const domain = normalizeHostname(rawDomain, false) + + if (!domain) { + throw new BadRequestError(`Invalid domain: ${rawDomain}`) + } - let organizationId: string | undefined + const qx = optionsQx(req) - await qx.tx(async (tx) => { + const organizationId = await qx.tx(async (tx) => { const orgSource = OrganizationAttributeSource.LFX_SERVE - organizationId = await findOrCreateOrganization(tx, orgSource, { + const organizationId = await findOrCreateOrganization(tx, orgSource, { displayName: name, logo, identities: [ @@ -59,6 +63,8 @@ export async function createOrganization(req: Request, res: Response): Promise Date: Wed, 8 Jul 2026 11:22:22 +0530 Subject: [PATCH 5/8] fix: correct type casting for organization domains in query Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com> --- services/libs/data-access-layer/src/members/organizations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/libs/data-access-layer/src/members/organizations.ts b/services/libs/data-access-layer/src/members/organizations.ts index a4d6c5a3fb..594ed21654 100644 --- a/services/libs/data-access-layer/src/members/organizations.ts +++ b/services/libs/data-access-layer/src/members/organizations.ts @@ -217,7 +217,7 @@ export async function fetchManyMemberOrgsWithOrgData( ): Promise> { const domainSelect = withDomains ? `, - COALESCE(oid.domains, '{}') AS "organizationDomains"` + COALESCE(oid.domains, '{}'::text[]) AS "organizationDomains"` : '' const domainJoin = withDomains From d90024a8c273a356e98d8eae6e55788909f14951 Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:59:20 +0530 Subject: [PATCH 6/8] fix: make URL cleaning case-insensitive for improved domain extraction Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com> --- services/libs/common/src/domain.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/libs/common/src/domain.ts b/services/libs/common/src/domain.ts index c215bc7b2f..77cd10b6cd 100644 --- a/services/libs/common/src/domain.ts +++ b/services/libs/common/src/domain.ts @@ -14,7 +14,7 @@ interface ParseResult { } export const cleanURL = (url: string): string => - url.replace(/^(?:https?:\/\/)?(?:www\.)?([^/]+)(?:\/.*)?$/, '$1') + url.replace(/^(?:https?:\/\/)?(?:www\.)?([^/]+)(?:\/.*)?$/i, '$1') export const isValidDomain = (parsed: ParseResult): boolean => Boolean(parsed.domain && parsed.isIcann) From fbbb8e955d55b20aa7859450f11c062ee1117021 Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Wed, 8 Jul 2026 12:09:41 +0530 Subject: [PATCH 7/8] fix: enhance domain validation in organization retrieval Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com> --- .../api/public/v1/organizations/getOrganization.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/backend/src/api/public/v1/organizations/getOrganization.ts b/backend/src/api/public/v1/organizations/getOrganization.ts index 7316770d7f..0b9a6147c0 100644 --- a/backend/src/api/public/v1/organizations/getOrganization.ts +++ b/backend/src/api/public/v1/organizations/getOrganization.ts @@ -1,7 +1,7 @@ import type { Request, Response } from 'express' import { z } from 'zod' -import { NotFoundError, normalizeHostname } from '@crowd/common' +import { BadRequestError, NotFoundError, normalizeHostname } from '@crowd/common' import { findOrganizationByNameOrDomain, optionsQx } from '@crowd/data-access-layer' import { ok } from '@/utils/api' @@ -17,12 +17,19 @@ const querySchema = z }) export async function getOrganization(req: Request, res: Response): Promise { - const { name, domain } = validateOrThrow(querySchema, req.query) + const { name, domain: rawDomain } = validateOrThrow(querySchema, req.query) + + const domain = rawDomain ? normalizeHostname(rawDomain, false) : undefined + + if (rawDomain && !domain) { + throw new BadRequestError(`Invalid domain: ${rawDomain}`) + } + const qx = optionsQx(req) const organization = await findOrganizationByNameOrDomain(qx, { name, - domain: domain ? normalizeHostname(domain, false) : undefined, + domain, }) if (!organization) { From 33a34a00eb8dc3001e9f65b49b1789e82f1786fa Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:28:02 +0530 Subject: [PATCH 8/8] fix: make prettier and linter happy Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com> --- backend/src/api/public/v1/organizations/getOrganization.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/api/public/v1/organizations/getOrganization.ts b/backend/src/api/public/v1/organizations/getOrganization.ts index 0b9a6147c0..0926f30471 100644 --- a/backend/src/api/public/v1/organizations/getOrganization.ts +++ b/backend/src/api/public/v1/organizations/getOrganization.ts @@ -20,7 +20,7 @@ export async function getOrganization(req: Request, res: Response): Promise