From aa776226b0db9ab58ef80ef3ace255162d737355 Mon Sep 17 00:00:00 2001 From: Kelly Date: Tue, 9 Dec 2025 10:28:18 -0700 Subject: [PATCH] fix(consumer): Wire findagram/findadispo to public API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update Dockerfiles to use cannaiq.co as API base URL - Change findagram API client from /api/az to /api/v1 endpoints - Add trusted origin bypass in public-api middleware for consumer sites - Consumer sites (findagram.co, findadispo.com) can now access /api/v1 endpoints without API key authentication 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- backend/src/routes/public-api.ts | 42 ++++++++++++++++++++++++++++ findadispo/frontend/Dockerfile | 3 +- findagram/frontend/Dockerfile | 3 +- findagram/frontend/src/api/client.js | 30 ++++++++++---------- 4 files changed, 61 insertions(+), 17 deletions(-) diff --git a/backend/src/routes/public-api.ts b/backend/src/routes/public-api.ts index f8736ca4..9d524342 100644 --- a/backend/src/routes/public-api.ts +++ b/backend/src/routes/public-api.ts @@ -120,6 +120,35 @@ function isDomainAllowed(origin: string, allowedDomains: string[]): boolean { } } +// Trusted origins for consumer sites (bypass API key auth) +const CONSUMER_TRUSTED_ORIGINS = [ + 'https://findagram.co', + 'https://www.findagram.co', + 'https://findadispo.com', + 'https://www.findadispo.com', + 'http://localhost:3001', + 'http://localhost:3002', +]; + +/** + * Check if request is from a trusted consumer origin + */ +function isConsumerTrustedRequest(req: Request): boolean { + const origin = req.headers.origin; + if (origin && CONSUMER_TRUSTED_ORIGINS.includes(origin)) { + return true; + } + const referer = req.headers.referer; + if (referer) { + for (const trusted of CONSUMER_TRUSTED_ORIGINS) { + if (referer.startsWith(trusted)) { + return true; + } + } + } + return false; +} + /** * Middleware to validate API key and build scope */ @@ -128,6 +157,19 @@ async function validatePublicApiKey( res: Response, next: NextFunction ) { + // Allow trusted consumer origins without API key (read-only access to all dispensaries) + if (isConsumerTrustedRequest(req)) { + // Create a synthetic internal permission for consumer sites + req.scope = { + type: 'internal', + dispensaryIds: 'ALL', + apiKeyId: 0, + apiKeyName: 'consumer-site', + rateLimit: 100, + }; + return next(); + } + const apiKey = req.headers['x-api-key'] as string; if (!apiKey) { diff --git a/findadispo/frontend/Dockerfile b/findadispo/frontend/Dockerfile index dffca9c6..6a41a352 100644 --- a/findadispo/frontend/Dockerfile +++ b/findadispo/frontend/Dockerfile @@ -13,7 +13,8 @@ RUN npm install COPY . . # Set build-time environment variable for API URL (CRA uses REACT_APP_ prefix) -ENV REACT_APP_API_URL=https://api.findadispo.com +# All consumer sites use cannaiq.co/api as the backend +ENV REACT_APP_API_URL=https://cannaiq.co # Build the app (CRA produces /build, not /dist) RUN npm run build diff --git a/findagram/frontend/Dockerfile b/findagram/frontend/Dockerfile index 70751a64..6a41a352 100644 --- a/findagram/frontend/Dockerfile +++ b/findagram/frontend/Dockerfile @@ -13,7 +13,8 @@ RUN npm install COPY . . # Set build-time environment variable for API URL (CRA uses REACT_APP_ prefix) -ENV REACT_APP_API_URL=https://api.findagram.co +# All consumer sites use cannaiq.co/api as the backend +ENV REACT_APP_API_URL=https://cannaiq.co # Build the app (CRA produces /build, not /dist) RUN npm run build diff --git a/findagram/frontend/src/api/client.js b/findagram/frontend/src/api/client.js index de37ddbe..e506d93a 100644 --- a/findagram/frontend/src/api/client.js +++ b/findagram/frontend/src/api/client.js @@ -1,11 +1,11 @@ /** * Findagram API Client * - * Connects to the backend /api/az/* endpoints which are publicly accessible. + * Connects to the backend /api/v1/* public endpoints. * Uses REACT_APP_API_URL environment variable for the base URL. * * Local development: http://localhost:3010 - * Production: https://findagram.co (proxied to backend via ingress) + * Production: https://cannaiq.co (shared API backend) */ const API_BASE_URL = process.env.REACT_APP_API_URL || ''; @@ -70,14 +70,14 @@ export async function getProducts(params = {}) { offset: params.offset || 0, }); - return request(`/api/az/products${queryString}`); + return request(`/api/v1/products${queryString}`); } /** * Get a single product by ID */ export async function getProduct(id) { - return request(`/api/az/products/${id}`); + return request(`/api/v1/products/${id}`); } /** @@ -103,7 +103,7 @@ export async function getProductAvailability(productId, params = {}) { max_radius_miles: maxRadiusMiles, }); - return request(`/api/az/products/${productId}/availability${queryString}`); + return request(`/api/v1/products/${productId}/availability${queryString}`); } /** @@ -113,7 +113,7 @@ export async function getProductAvailability(productId, params = {}) { * @returns {Promise<{similarProducts: Array<{productId: number, name: string, brandName: string, imageUrl: string, price: number}>}>} */ export async function getSimilarProducts(productId) { - return request(`/api/az/products/${productId}/similar`); + return request(`/api/v1/products/${productId}/similar`); } /** @@ -130,7 +130,7 @@ export async function getStoreProducts(storeId, params = {}) { offset: params.offset || 0, }); - return request(`/api/az/stores/${storeId}/products${queryString}`); + return request(`/api/v1/stores/${storeId}/products${queryString}`); } // ============================================================ @@ -154,42 +154,42 @@ export async function getDispensaries(params = {}) { offset: params.offset || 0, }); - return request(`/api/az/stores${queryString}`); + return request(`/api/v1/stores${queryString}`); } /** * Get a single dispensary by ID */ export async function getDispensary(id) { - return request(`/api/az/stores/${id}`); + return request(`/api/v1/stores/${id}`); } /** * Get dispensary by slug or platform ID */ export async function getDispensaryBySlug(slug) { - return request(`/api/az/stores/slug/${slug}`); + return request(`/api/v1/stores/slug/${slug}`); } /** * Get dispensary summary (product counts, categories, brands) */ export async function getDispensarySummary(id) { - return request(`/api/az/stores/${id}/summary`); + return request(`/api/v1/stores/${id}/summary`); } /** * Get brands available at a specific dispensary */ export async function getDispensaryBrands(id) { - return request(`/api/az/stores/${id}/brands`); + return request(`/api/v1/stores/${id}/brands`); } /** * Get categories available at a specific dispensary */ export async function getDispensaryCategories(id) { - return request(`/api/az/stores/${id}/categories`); + return request(`/api/v1/stores/${id}/categories`); } // ============================================================ @@ -200,7 +200,7 @@ export async function getDispensaryCategories(id) { * Get all categories with product counts */ export async function getCategories() { - return request('/api/az/categories'); + return request('/api/v1/categories'); } // ============================================================ @@ -220,7 +220,7 @@ export async function getBrands(params = {}) { offset: params.offset || 0, }); - return request(`/api/az/brands${queryString}`); + return request(`/api/v1/brands${queryString}`); } // ============================================================