fix(consumer): Wire findagram/findadispo to public API
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
* Middleware to validate API key and build scope
|
||||||
*/
|
*/
|
||||||
@@ -128,6 +157,19 @@ async function validatePublicApiKey(
|
|||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
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;
|
const apiKey = req.headers['x-api-key'] as string;
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ RUN npm install
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Set build-time environment variable for API URL (CRA uses REACT_APP_ prefix)
|
# 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)
|
# Build the app (CRA produces /build, not /dist)
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ RUN npm install
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Set build-time environment variable for API URL (CRA uses REACT_APP_ prefix)
|
# 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)
|
# Build the app (CRA produces /build, not /dist)
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* Findagram API Client
|
* 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.
|
* Uses REACT_APP_API_URL environment variable for the base URL.
|
||||||
*
|
*
|
||||||
* Local development: http://localhost:3010
|
* 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 || '';
|
const API_BASE_URL = process.env.REACT_APP_API_URL || '';
|
||||||
@@ -70,14 +70,14 @@ export async function getProducts(params = {}) {
|
|||||||
offset: params.offset || 0,
|
offset: params.offset || 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
return request(`/api/az/products${queryString}`);
|
return request(`/api/v1/products${queryString}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a single product by ID
|
* Get a single product by ID
|
||||||
*/
|
*/
|
||||||
export async function getProduct(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,
|
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}>}>}
|
* @returns {Promise<{similarProducts: Array<{productId: number, name: string, brandName: string, imageUrl: string, price: number}>}>}
|
||||||
*/
|
*/
|
||||||
export async function getSimilarProducts(productId) {
|
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,
|
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,
|
offset: params.offset || 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
return request(`/api/az/stores${queryString}`);
|
return request(`/api/v1/stores${queryString}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a single dispensary by ID
|
* Get a single dispensary by ID
|
||||||
*/
|
*/
|
||||||
export async function getDispensary(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
|
* Get dispensary by slug or platform ID
|
||||||
*/
|
*/
|
||||||
export async function getDispensaryBySlug(slug) {
|
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)
|
* Get dispensary summary (product counts, categories, brands)
|
||||||
*/
|
*/
|
||||||
export async function getDispensarySummary(id) {
|
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
|
* Get brands available at a specific dispensary
|
||||||
*/
|
*/
|
||||||
export async function getDispensaryBrands(id) {
|
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
|
* Get categories available at a specific dispensary
|
||||||
*/
|
*/
|
||||||
export async function getDispensaryCategories(id) {
|
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
|
* Get all categories with product counts
|
||||||
*/
|
*/
|
||||||
export async function getCategories() {
|
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,
|
offset: params.offset || 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
return request(`/api/az/brands${queryString}`);
|
return request(`/api/v1/brands${queryString}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user