diff --git a/apps/candidate/src/app/portal/messages/components/thread/thread-panel.tsx b/apps/candidate/src/app/portal/messages/components/thread/thread-panel.tsx index dc8f89e03..389f6cc9e 100644 --- a/apps/candidate/src/app/portal/messages/components/thread/thread-panel.tsx +++ b/apps/candidate/src/app/portal/messages/components/thread/thread-panel.tsx @@ -121,7 +121,7 @@ export default function ThreadPanel({ if (companyId) { try { const response: any = await client.get( - `/companies/${companyId}`, + `/companies/${companyId}/view/summary`, ); const company = response?.data; if (company?.name) setCompanyName(company.name); diff --git a/apps/portal/src/components/chat/thread-panel.tsx b/apps/portal/src/components/chat/thread-panel.tsx index 3075e3361..5e119da39 100644 --- a/apps/portal/src/components/chat/thread-panel.tsx +++ b/apps/portal/src/components/chat/thread-panel.tsx @@ -135,7 +135,7 @@ export default function ThreadPanel({ conversationId }: ThreadPanelProps) { if (companyId) { try { const response: any = await client.get( - `/companies/${companyId}`, + `/companies/${companyId}/view/summary`, ); const company = response?.data; if (company?.name) setCompanyName(company.name); diff --git a/services/api-gateway/src/routes/v3/ats.ts b/services/api-gateway/src/routes/v3/ats.ts index 351dccb18..ecc86c7ff 100644 --- a/services/api-gateway/src/routes/v3/ats.ts +++ b/services/api-gateway/src/routes/v3/ats.ts @@ -97,6 +97,7 @@ const atsV3Routes: V3RouteConfig[] = [ // ── Companies Views ─────────────────────────────────────────── { path: '/companies/:id/view/contacts', method: 'GET', auth: 'required' }, + { path: '/companies/:id/view/summary', method: 'GET', auth: 'required' }, // ── Applications Core CRUD ──────────────────────────────────── { resource: 'applications', auth: 'required' }, diff --git a/services/ats-service/src/v3/companies/routes.ts b/services/ats-service/src/v3/companies/routes.ts index 811c45114..4c29d1f14 100644 --- a/services/ats-service/src/v3/companies/routes.ts +++ b/services/ats-service/src/v3/companies/routes.ts @@ -8,6 +8,7 @@ import { IEventPublisher } from '../../v2/shared/events.js'; import { CompanyRepository } from './repository.js'; import { CompanyService } from './service.js'; import { registerCompanyContactsView } from './views/contacts.route.js'; +import { registerCompanySummaryView } from './views/summary.route.js'; import { CreateCompanyInput, UpdateCompanyInput, @@ -28,6 +29,7 @@ export function registerCompanyRoutes( // --- Views (registered before :id routes to avoid collision) --- registerCompanyContactsView(app, supabase); + registerCompanySummaryView(app, supabase); // --- Core 5 CRUD --- diff --git a/services/ats-service/src/v3/companies/views/summary.repository.ts b/services/ats-service/src/v3/companies/views/summary.repository.ts new file mode 100644 index 000000000..608cf2e19 --- /dev/null +++ b/services/ats-service/src/v3/companies/views/summary.repository.ts @@ -0,0 +1,33 @@ +/** + * Company Summary View Repository + * + * Flat projection of public display fields for a single company. + * Used by any authenticated context (candidate chat, marketplace, etc.) + * that needs to render a company's name/logo without full CRUD access. + */ + +import { SupabaseClient } from '@supabase/supabase-js'; + +export interface CompanySummary { + id: string; + name: string | null; + logo_url: string | null; + website: string | null; + industry: string | null; + headquarters_location: string | null; +} + +export class CompanySummaryRepository { + constructor(private supabase: SupabaseClient) {} + + async findById(companyId: string): Promise { + const { data, error } = await this.supabase + .from('companies') + .select('id, name, logo_url, website, industry, headquarters_location') + .eq('id', companyId) + .maybeSingle(); + + if (error) throw error; + return (data as CompanySummary) ?? null; + } +} diff --git a/services/ats-service/src/v3/companies/views/summary.route.ts b/services/ats-service/src/v3/companies/views/summary.route.ts new file mode 100644 index 000000000..fc1ead61c --- /dev/null +++ b/services/ats-service/src/v3/companies/views/summary.route.ts @@ -0,0 +1,29 @@ +/** + * GET /api/v3/companies/:id/view/summary + * + * Returns public display fields (name, logo, website, industry, HQ location) + * for a company. Available to any authenticated user — no access control. + */ + +import { FastifyInstance } from 'fastify'; +import { SupabaseClient } from '@supabase/supabase-js'; +import { CompanySummaryRepository } from './summary.repository.js'; +import { CompanySummaryService } from './summary.service.js'; +import { idParamSchema } from '../types.js'; + +export function registerCompanySummaryView(app: FastifyInstance, supabase: SupabaseClient) { + const repository = new CompanySummaryRepository(supabase); + const service = new CompanySummaryService(repository); + + app.get('/api/v3/companies/:id/view/summary', { + schema: { params: idParamSchema }, + }, async (request, reply) => { + const clerkUserId = request.headers['x-clerk-user-id'] as string; + if (!clerkUserId) { + return reply.status(401).send({ error: { code: 'AUTH_REQUIRED', message: 'Authentication required' } }); + } + const { id } = request.params as { id: string }; + const data = await service.getById(id); + return reply.send({ data }); + }); +} diff --git a/services/ats-service/src/v3/companies/views/summary.service.ts b/services/ats-service/src/v3/companies/views/summary.service.ts new file mode 100644 index 000000000..ba0e2ab25 --- /dev/null +++ b/services/ats-service/src/v3/companies/views/summary.service.ts @@ -0,0 +1,21 @@ +/** + * Company Summary View Service + * + * Returns public display fields for a company. Available to any authenticated + * user — candidates, recruiters, company users, admins — so that chat, marketplace, + * and other cross-role surfaces can render a company's name and logo without + * hitting the access-scoped CRUD route. + */ + +import { NotFoundError } from '@splits-network/shared-fastify'; +import { CompanySummaryRepository, CompanySummary } from './summary.repository.js'; + +export class CompanySummaryService { + constructor(private repository: CompanySummaryRepository) {} + + async getById(companyId: string): Promise { + const summary = await this.repository.findById(companyId); + if (!summary) throw new NotFoundError('Company', companyId); + return summary; + } +}