fix: prevent duplicate SIP registration (module-level guard), extract real caller number from X-CALLERNO header

This commit is contained in:
2026-03-18 08:33:45 +05:30
parent dfc4a0cd44
commit 125aeae41c
2 changed files with 17 additions and 12 deletions

View File

@@ -223,10 +223,17 @@ export class SIPClient {
} }
private extractCallerNumber(session: RTCSession): string { private extractCallerNumber(session: RTCSession): string {
// Try P-Asserted-Identity header first (most reliable for real caller ID)
try { try {
const request = session.direction === 'incoming' ? (session as any)._request : null; const request = session.direction === 'incoming' ? (session as any)._request : null;
if (request) { if (request) {
// Ozonetel sends the real caller number in X-CALLERNO header
const xCallerNo = request.getHeader('X-CALLERNO');
if (xCallerNo) {
// Remove leading 0s or country code prefix (00919... → 919...)
const cleaned = xCallerNo.replace(/^0+/, '');
return cleaned;
}
// Check P-Asserted-Identity // Check P-Asserted-Identity
const pai = request.getHeader('P-Asserted-Identity'); const pai = request.getHeader('P-Asserted-Identity');
if (pai) { if (pai) {
@@ -241,11 +248,11 @@ export class SIPClient {
if (match) return match[1]; if (match) return match[1];
} }
// Check X-Original-CallerID (custom header some PBX systems use) // Check X-Original-CallerID
const xCid = request.getHeader('X-Original-CallerID'); const xCid = request.getHeader('X-Original-CallerID');
if (xCid) return xCid; if (xCid) return xCid;
// Check From header display name (sometimes contains the real number) // Check From header display name
const fromDisplay = request.from?.display_name; const fromDisplay = request.from?.display_name;
if (fromDisplay && /^\+?\d{7,}$/.test(fromDisplay)) { if (fromDisplay && /^\+?\d{7,}$/.test(fromDisplay)) {
return fromDisplay; return fromDisplay;

View File

@@ -1,4 +1,4 @@
import { createContext, useContext, useEffect, useRef, useState, type PropsWithChildren } from 'react'; import { createContext, useContext, useEffect, useState, type PropsWithChildren } from 'react';
import { useSipPhone } from '@/hooks/use-sip-phone'; import { useSipPhone } from '@/hooks/use-sip-phone';
type SipContextType = ReturnType<typeof useSipPhone> & { type SipContextType = ReturnType<typeof useSipPhone> & {
@@ -8,22 +8,20 @@ type SipContextType = ReturnType<typeof useSipPhone> & {
const SipContext = createContext<SipContextType | null>(null); const SipContext = createContext<SipContextType | null>(null);
// Module-level flag — survives React StrictMode double-mount
let sipConnectedGlobal = false;
export const SipProvider = ({ children }: PropsWithChildren) => { export const SipProvider = ({ children }: PropsWithChildren) => {
const sipPhone = useSipPhone(); const sipPhone = useSipPhone();
const hasConnected = useRef(false);
const [ozonetelStatus, setOzonetelStatus] = useState<'idle' | 'logging-in' | 'logged-in' | 'error'>('idle'); const [ozonetelStatus, setOzonetelStatus] = useState<'idle' | 'logging-in' | 'logged-in' | 'error'>('idle');
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [ozonetelError, _setOzonetelError] = useState<string | null>(null); const [ozonetelError, _setOzonetelError] = useState<string | null>(null);
// Auto-connect SIP on mount — only WebSocket/SIP registration // Auto-connect SIP on mount — module-level guard prevents duplicate connections
// Ozonetel agent login is handled by the sidecar during auth, NOT here
useEffect(() => { useEffect(() => {
if (!hasConnected.current) { if (!sipConnectedGlobal) {
hasConnected.current = true; sipConnectedGlobal = true;
sipPhone.connect(); sipPhone.connect();
// Ozonetel status tracks whether the sidecar handled agent login
// We assume logged-in if SIP connects (sidecar handles the REST login)
setOzonetelStatus('logged-in'); setOzonetelStatus('logged-in');
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps