Changes to extract company ID from path

This commit is contained in:
Ritul-Work 2026-03-26 19:15:08 +05:30
parent 83b48a3aca
commit 388ede765a
7 changed files with 189 additions and 32 deletions

View File

@ -1,8 +1,21 @@
import axios from 'axios';
import { getRuntimeApplicationId, getRuntimeCompanyId } from '../utils/runtimeContext';
const apiClient = axios.create({
baseURL: '/',
headers: { 'Content-Type': 'application/json' },
});
apiClient.interceptors.request.use((config) => {
const companyId = getRuntimeCompanyId();
const applicationId = getRuntimeApplicationId();
const headers = config.headers || {};
if (companyId) headers['x-company-id'] = companyId;
if (applicationId) headers['x-application-id'] = applicationId;
config.headers = headers;
return config;
});
export default apiClient;

View File

@ -1,7 +1,7 @@
import { useState, useEffect } from 'react';
import apiClient from '../api/client';
export default function ImagePicker({ slug, currentImage, onSelect }) {
export default function ImagePicker({ currentImage, onSelect }) {
const [images, setImages] = useState([]);
const [loading, setLoading] = useState(true);

View File

@ -1,3 +1,4 @@
/* eslint-disable react-refresh/only-export-components */
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
import apiClient from '../api/client';

View File

@ -1,5 +1,7 @@
/* eslint-disable react-refresh/only-export-components */
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
import apiClient from '../api/client';
import { getRuntimeCompanyId } from '../utils/runtimeContext';
const BusinessContext = createContext(null);
@ -16,14 +18,23 @@ export function BusinessProvider({ children }) {
const stored = sessionStorage.getItem(SESSION_KEY);
if (!stored) { setLoading(false); return; }
try {
const { businessId } = JSON.parse(stored);
const { businessId, companyId } = JSON.parse(stored);
const runtimeCompanyId = getRuntimeCompanyId();
if (runtimeCompanyId && companyId && runtimeCompanyId !== companyId) {
throw new Error('Stored business belongs to a different company context');
}
const [bizRes, smsRes] = await Promise.all([
apiClient.get(`/api/businesses/${businessId}`),
apiClient.get(`/api/businesses/${businessId}/global-sms/active`).catch(() => ({ data: {} }))
]);
setActiveBusinessState(bizRes.data);
setHasGlobalSms(!!smsRes.data?.activeProfile);
sessionStorage.setItem(SESSION_KEY, JSON.stringify({ businessId }));
sessionStorage.setItem(SESSION_KEY, JSON.stringify({
businessId,
companyId: runtimeCompanyId || companyId || '',
}));
} catch {
// Business no longer exists clear stale session
sessionStorage.removeItem(SESSION_KEY);
@ -38,7 +49,10 @@ export function BusinessProvider({ children }) {
const setActiveBusiness = useCallback(async (business) => {
setActiveBusinessState(business);
sessionStorage.setItem(SESSION_KEY, JSON.stringify({ businessId: business.businessId }));
sessionStorage.setItem(SESSION_KEY, JSON.stringify({
businessId: business.businessId,
companyId: getRuntimeCompanyId(),
}));
try {
const smsRes = await apiClient.get(`/api/businesses/${business.businessId}/global-sms/active`);
setHasGlobalSms(!!smsRes.data?.activeProfile);

View File

@ -1,5 +1,5 @@
import { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { useBrand } from '../context/BrandContext';
import RegisterBusinessModal from '../components/RegisterBusinessModal';
import apiClient from '../api/client';
@ -44,7 +44,6 @@ function DeleteConfirmModal({ brandName, onCancel, onConfirm, deleting }) {
export default function Brand() {
const { brand, loading, refetch } = useBrand();
const navigate = useNavigate();
const [showModal, setShowModal] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [deleting, setDeleting] = useState(false);

View File

@ -0,0 +1,39 @@
function getRuntimeUrl() {
if (typeof window === 'undefined') return null;
try {
return new URL(window.location.href);
} catch {
return null;
}
}
function getPathMatch(pathname, regex) {
const match = pathname.match(regex);
return match?.[1] || '';
}
export function getRuntimeCompanyId() {
const runtimeUrl = getRuntimeUrl();
if (!runtimeUrl) return '';
return (
runtimeUrl.searchParams.get('companyId')
|| runtimeUrl.searchParams.get('company_id')
|| getPathMatch(runtimeUrl.pathname, /\/company\/([^/]+)/i)
|| ''
).trim();
}
export function getRuntimeApplicationId() {
const runtimeUrl = getRuntimeUrl();
if (!runtimeUrl) return '';
return (
runtimeUrl.searchParams.get('applicationId')
|| runtimeUrl.searchParams.get('application_id')
|| getPathMatch(runtimeUrl.pathname, /\/application\/([^/]+)/i)
|| getPathMatch(runtimeUrl.pathname, /\/applications\/([^/]+)/i)
|| ''
).trim();
}

View File

@ -17,6 +17,31 @@ const axios = require('axios');
const MERCHANT_ID = () => process.env.MERCHANT_ID;
function normalizeScopeId(value) {
return typeof value === 'string' ? value.trim() : '';
}
function getCompanyId(req) {
return normalizeScopeId(
req.get('x-company-id')
|| req.query?.companyId
|| req.query?.company_id
|| req.body?.companyId
|| req.body?.company_id
|| MERCHANT_ID()
);
}
function getApplicationId(req) {
return normalizeScopeId(
req.get('x-application-id')
|| req.query?.applicationId
|| req.query?.application_id
|| req.body?.applicationId
|| req.body?.application_id
);
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
function slugify(text) {
@ -40,6 +65,18 @@ async function saveIndex(merchantId, businesses) {
await uploadJSON(indexPath(merchantId), 'index', { businesses });
}
async function findBusinessByApplicationId(merchantId, applicationId) {
const normalizedApplicationId = normalizeScopeId(applicationId);
if (!normalizedApplicationId) return null;
const businesses = await getIndex(merchantId);
return businesses.find((business) => {
const storedApplicationId = normalizeScopeId(business.applicationId);
const storedBusinessId = normalizeScopeId(business.businessId);
return storedApplicationId === normalizedApplicationId || storedBusinessId === normalizedApplicationId;
}) || null;
}
const PROVIDER_FIELDS = ['providerName', 'senderId', 'dltEntityId', 'authKey'];
function createHttpError(status, message, extra = {}) {
@ -67,6 +104,10 @@ function normalizeSenderId(value) {
return normalizeText(value).toUpperCase();
}
function renderTemplateWithUsername(template, username) {
return String(template || '').replace(/\{#var#\}/g, normalizeText(username));
}
function isValidCurlCommand(rawCurl) {
return normalizeText(rawCurl).toLowerCase().startsWith('curl');
}
@ -239,7 +280,7 @@ function getMissingMandatoryProviderFields(provider = {}) {
// GET /api/businesses
router.get('/', async (req, res) => {
try {
const businesses = await getIndex(MERCHANT_ID());
const businesses = await getIndex(getCompanyId(req));
res.json({ businesses });
} catch (err) {
res.status(500).json({ error: err.message });
@ -252,7 +293,14 @@ router.post('/', async (req, res) => {
const { websiteUrl } = req.body;
if (!websiteUrl) return res.status(400).json({ error: 'websiteUrl is required' });
const merchantId = MERCHANT_ID();
const merchantId = getCompanyId(req);
const applicationId = getApplicationId(req);
const businesses = await getIndex(merchantId);
if (applicationId && businesses.some((business) => normalizeScopeId(business.applicationId) === applicationId)) {
return res.status(409).json({ error: 'A business is already configured for this applicationId' });
}
const businessId = uuidv4();
const bizRoot = businessRoot(merchantId, businessId);
const imagesFolder = `${bizRoot}/images`;
@ -277,6 +325,8 @@ router.post('/', async (req, res) => {
const contextJson = {
businessId,
merchantId,
companyId: merchantId,
applicationId,
domain,
brandName: brandContext.brandName || 'Unknown Brand',
tone: brandContext.tone || 'professional',
@ -292,9 +342,10 @@ router.post('/', async (req, res) => {
await uploadJSON(bizRoot, 'events', { events: DEFAULT_EVENTS });
// 6. Update index.json
const businesses = await getIndex(merchantId);
businesses.push({
businessId,
companyId: merchantId,
applicationId,
brandName: contextJson.brandName,
domain: contextJson.domain,
createdAt: contextJson.createdAt,
@ -313,7 +364,7 @@ router.post('/', async (req, res) => {
router.get('/:businessId', async (req, res) => {
try {
const { businessId } = req.params;
const context = await fetchJSON(businessRoot(MERCHANT_ID(), businessId), 'context');
const context = await fetchJSON(businessRoot(getCompanyId(req), businessId), 'context');
if (!context) return res.status(404).json({ error: 'Business not found' });
res.json(context);
} catch (err) {
@ -324,7 +375,7 @@ router.get('/:businessId', async (req, res) => {
// DELETE /api/businesses/:businessId
router.delete('/:businessId', async (req, res) => {
try {
const merchantId = MERCHANT_ID();
const merchantId = getCompanyId(req);
const { businessId } = req.params;
await deleteBusinessFiles(merchantId, businessId);
@ -340,12 +391,52 @@ router.delete('/:businessId', async (req, res) => {
}
});
// POST /api/businesses/resolve-template
router.post('/resolve-template', async (req, res) => {
try {
const companyId = getCompanyId(req);
const applicationId = getApplicationId(req);
const event = normalizeText(req.body?.event);
const username = normalizeText(req.body?.username);
if (!companyId) return res.status(400).json({ error: 'companyId is required' });
if (!applicationId) return res.status(400).json({ error: 'applicationId is required' });
if (!event) return res.status(400).json({ error: 'event is required' });
if (!username) return res.status(400).json({ error: 'username is required' });
const business = await findBusinessByApplicationId(companyId, applicationId);
if (!business) {
return res.status(404).json({ error: 'Business not found for applicationId' });
}
const eventSlug = slugify(event);
const folder = `${businessRoot(companyId, business.businessId)}/templates`;
const tmpl = await fetchJSON(folder, eventSlug);
if (!tmpl || tmpl.status !== 'whitelisted' || !normalizeText(tmpl.selectedTemplate)) {
return res.status(404).json({ error: 'Whitelisted template not found' });
}
res.json({
success: true,
companyId,
applicationId,
event: eventSlug,
username,
templateId: normalizeText(tmpl.templateId),
template: renderTemplateWithUsername(tmpl.selectedTemplate, username),
});
} catch (err) {
sendRouteError(res, err);
}
});
// ─── Providers ────────────────────────────────────────────────────────────────
// GET /api/businesses/:businessId/providers
router.get('/:businessId/providers', async (req, res) => {
try {
const bizRoot = businessRoot(MERCHANT_ID(), req.params.businessId);
const bizRoot = businessRoot(getCompanyId(req), req.params.businessId);
const activeProfile = await getActiveProfile(bizRoot);
if (!activeProfile) {
return res.status(400).json({ error: 'An active cURL profile is required before editing provider settings.' });
@ -360,7 +451,7 @@ router.get('/:businessId/providers', async (req, res) => {
// POST /api/businesses/:businessId/providers
router.post('/:businessId/providers', async (req, res) => {
try {
const bizRoot = businessRoot(MERCHANT_ID(), req.params.businessId);
const bizRoot = businessRoot(getCompanyId(req), req.params.businessId);
const providerPatch = getProviderPatch(req.body);
const senderIdError = validateSenderId(providerPatch?.senderId || '');
if (senderIdError) {
@ -388,7 +479,7 @@ router.post('/:businessId/providers', async (req, res) => {
// GET /api/businesses/:businessId/global-sms
router.get('/:businessId/global-sms', async (req, res) => {
try {
const bizRoot = businessRoot(MERCHANT_ID(), req.params.businessId);
const bizRoot = businessRoot(getCompanyId(req), req.params.businessId);
const activeProfile = await getActiveProfile(bizRoot);
res.json(activeProfile ? { rawCurl: activeProfile.rawCurl, updatedAt: activeProfile.updatedAt } : {});
} catch (err) {
@ -408,7 +499,7 @@ router.post('/:businessId/global-sms', async (req, res) => {
return res.status(400).json({ error: 'rawCurl must be a valid cURL command' });
}
const bizRoot = businessRoot(MERCHANT_ID(), req.params.businessId);
const bizRoot = businessRoot(getCompanyId(req), req.params.businessId);
const { profileData } = await getProfileState(bizRoot);
const now = new Date().toISOString();
const normalizedCurl = normalizeText(rawCurl);
@ -446,7 +537,7 @@ router.post('/:businessId/global-sms', async (req, res) => {
// GET /api/businesses/:businessId/global-sms/profiles
router.get('/:businessId/global-sms/profiles', async (req, res) => {
try {
const bizRoot = businessRoot(MERCHANT_ID(), req.params.businessId);
const bizRoot = businessRoot(getCompanyId(req), req.params.businessId);
const { profileData, activeProfileId } = await getProfileState(bizRoot);
const profiles = profileData.profiles || [];
res.json({ profiles, activeProfileId });
@ -469,7 +560,7 @@ router.post('/:businessId/global-sms/profiles', async (req, res) => {
return res.status(400).json({ error: 'rawCurl must be a valid cURL command' });
}
const bizRoot = businessRoot(MERCHANT_ID(), req.params.businessId);
const bizRoot = businessRoot(getCompanyId(req), req.params.businessId);
const { profileData } = await getProfileState(bizRoot);
const now = new Date().toISOString();
const normalizedCurl = normalizeText(rawCurl);
@ -519,7 +610,7 @@ router.patch('/:businessId/global-sms/profiles/:profileId', async (req, res) =>
return res.status(400).json({ error: senderIdError });
}
const bizRoot = businessRoot(MERCHANT_ID(), businessId);
const bizRoot = businessRoot(getCompanyId(req), businessId);
const { profileData } = await getProfileState(bizRoot);
const profile = profileData.profiles.find(p => p.id === profileId);
if (!profile) return res.status(404).json({ error: 'Profile not found' });
@ -539,7 +630,7 @@ router.patch('/:businessId/global-sms/profiles/:profileId', async (req, res) =>
router.delete('/:businessId/global-sms/profiles/:profileId', async (req, res) => {
try {
const { businessId, profileId } = req.params;
const bizRoot = businessRoot(MERCHANT_ID(), businessId);
const bizRoot = businessRoot(getCompanyId(req), businessId);
const { profileData } = await getProfileState(bizRoot);
const idx = profileData.profiles.findIndex(p => p.id === profileId);
@ -567,7 +658,7 @@ router.delete('/:businessId/global-sms/profiles/:profileId', async (req, res) =>
router.post('/:businessId/global-sms/profiles/:profileId/activate', async (req, res) => {
try {
const { businessId, profileId } = req.params;
const bizRoot = businessRoot(MERCHANT_ID(), businessId);
const bizRoot = businessRoot(getCompanyId(req), businessId);
const { profileData } = await getProfileState(bizRoot);
const profile = profileData.profiles.find(p => p.id === profileId);
if (!profile) return res.status(404).json({ error: 'Profile not found' });
@ -582,7 +673,7 @@ router.post('/:businessId/global-sms/profiles/:profileId/activate', async (req,
// GET /api/businesses/:businessId/global-sms/active
router.get('/:businessId/global-sms/active', async (req, res) => {
try {
const bizRoot = businessRoot(MERCHANT_ID(), req.params.businessId);
const bizRoot = businessRoot(getCompanyId(req), req.params.businessId);
const { activeProfile, activeProfileId } = await getProfileState(bizRoot);
res.json({ activeProfile, activeProfileId });
} catch (err) {
@ -595,7 +686,7 @@ router.get('/:businessId/global-sms/active', async (req, res) => {
// GET /api/businesses/:businessId/events
router.get('/:businessId/events', async (req, res) => {
try {
const data = await fetchJSON(businessRoot(MERCHANT_ID(), req.params.businessId), 'events');
const data = await fetchJSON(businessRoot(getCompanyId(req), req.params.businessId), 'events');
res.json(data || { events: DEFAULT_EVENTS });
} catch (err) {
res.status(500).json({ error: err.message });
@ -608,7 +699,7 @@ router.post('/:businessId/events', async (req, res) => {
const { label } = req.body;
if (!label) return res.status(400).json({ error: 'label is required' });
const bizRoot = businessRoot(MERCHANT_ID(), req.params.businessId);
const bizRoot = businessRoot(getCompanyId(req), req.params.businessId);
const data = await fetchJSON(bizRoot, 'events') || { events: [...DEFAULT_EVENTS] };
const slug = slugify(label);
@ -629,7 +720,7 @@ router.post('/:businessId/events', async (req, res) => {
router.delete('/:businessId/events/:slug', async (req, res) => {
try {
const { businessId, slug } = req.params;
const bizRoot = businessRoot(MERCHANT_ID(), businessId);
const bizRoot = businessRoot(getCompanyId(req), businessId);
const data = await fetchJSON(bizRoot, 'events') || { events: [...DEFAULT_EVENTS] };
const event = data.events.find(e => e.slug === slug);
@ -648,7 +739,7 @@ router.delete('/:businessId/events/:slug', async (req, res) => {
router.post('/:businessId/events/:slug/generate', async (req, res) => {
try {
const { businessId, slug } = req.params;
const bizRoot = businessRoot(MERCHANT_ID(), businessId);
const bizRoot = businessRoot(getCompanyId(req), businessId);
const context = await fetchJSON(bizRoot, 'context');
if (!context) return res.status(400).json({ error: 'Business context not found.' });
@ -692,7 +783,7 @@ router.post('/:businessId/events/:slug/generate', async (req, res) => {
// GET /api/businesses/:businessId/templates/images (must be before /:slug)
router.get('/:businessId/templates/images', async (req, res) => {
try {
const images = await listImages(`${businessRoot(MERCHANT_ID(), req.params.businessId)}/images`);
const images = await listImages(`${businessRoot(getCompanyId(req), req.params.businessId)}/images`);
res.json({ images });
} catch (err) {
res.status(500).json({ error: err.message });
@ -702,7 +793,7 @@ router.get('/:businessId/templates/images', async (req, res) => {
// GET /api/businesses/:businessId/templates
router.get('/:businessId/templates', async (req, res) => {
try {
const bizRoot = businessRoot(MERCHANT_ID(), req.params.businessId);
const bizRoot = businessRoot(getCompanyId(req), req.params.businessId);
const folder = `${bizRoot}/templates`;
const slugs = await listTemplateFiles(folder);
const templates = [];
@ -720,7 +811,7 @@ router.get('/:businessId/templates', async (req, res) => {
router.get('/:businessId/templates/:slug', async (req, res) => {
try {
const { businessId, slug } = req.params;
const tmpl = await fetchJSON(`${businessRoot(MERCHANT_ID(), businessId)}/templates`, slug);
const tmpl = await fetchJSON(`${businessRoot(getCompanyId(req), businessId)}/templates`, slug);
if (!tmpl) return res.status(404).json({ error: 'Template not found' });
res.json(tmpl);
} catch (err) {
@ -735,7 +826,7 @@ router.post('/:businessId/templates/:slug/select', async (req, res) => {
const { selectedVariant } = req.body;
if (!selectedVariant) return res.status(400).json({ error: 'selectedVariant is required' });
const bizRoot = businessRoot(MERCHANT_ID(), businessId);
const bizRoot = businessRoot(getCompanyId(req), businessId);
const folder = `${bizRoot}/templates`;
const tmpl = await fetchJSON(folder, slug);
@ -776,7 +867,7 @@ router.post('/:businessId/templates/:slug/whitelist', async (req, res) => {
return res.status(400).json({ error: 'templateId is required' });
}
const folder = `${businessRoot(MERCHANT_ID(), businessId)}/templates`;
const folder = `${businessRoot(getCompanyId(req), businessId)}/templates`;
const tmpl = await fetchJSON(folder, slug);
if (!tmpl) return res.status(404).json({ error: 'Template not found' });
if (tmpl.status !== 'pending_whitelisting') {
@ -809,7 +900,7 @@ router.post('/:businessId/templates/:slug/publish', async (req, res) => {
return res.status(400).json({ error: 'toNumber is required' });
}
const bizRoot = businessRoot(MERCHANT_ID(), businessId);
const bizRoot = businessRoot(getCompanyId(req), businessId);
const folder = `${bizRoot}/templates`;
// Load template
@ -877,7 +968,7 @@ router.post('/:businessId/templates/:slug/test', async (req, res) => {
const { toNumber } = req.body;
if (!normalizeText(toNumber)) return res.status(400).json({ error: 'toNumber is required' });
const bizRoot = businessRoot(MERCHANT_ID(), businessId);
const bizRoot = businessRoot(getCompanyId(req), businessId);
const folder = `${bizRoot}/templates`;
const tmpl = await fetchJSON(folder, slug);
if (!tmpl) return res.status(404).json({ error: 'Template not found' });