javascriptphplaravelvonage

Validate user when they try to call phone number with nexmo in laravel


i have customer service application using in app browser call using nexmo and laravel as framework. I have some users and each users have their list phone numbers in table and each row has contact action button to call. The target of phone number is put in button like this :

<a href="#" class="btn btn-sm btn-info call-button" data-toggle="modal" data-number="6281299054899">Contact</a>

but user can do inspect element and edit those button then put another phone numbers they want. How can i prevent this cheat and alert user in example "You have not authorize to call this phone number." when they do that action?

this my handler when .call-button is clicked:

dataTableAdminLeader.on('click', '.call-button', function(){
    let id                  = $(this).data('id');
    let master_number_id    = $(this).data('master_numbers_id');
    let target_number       = $(this).data('number');
    const trashButton       = document.querySelector('.btn-trash');
    const updateButton      = document.querySelector(".btn-update");

    trashButton.setAttribute('disabled', 'disabled');
    updateButton.setAttribute('disabled', 'disabled');

    $('#phone-call-modal').modal('show');
    // input reset
    document.querySelector('#target-phone').value = target_number;
    document.querySelector('#id-edit').value = id;
    document.querySelector('#master-numbers-id-edit').value = master_number_id;
    document.querySelector('#member-id').value = $(this).data('id-member');
    document.querySelector('#member-name').value = $(this).data('name');

    trashButton.removeAttribute('disabled');
    updateButton.removeAttribute('disabled');

    // all constant needed for call
    const USER_JWT          = "{{ $jwt_token }}";
    const phoneNumberInput  = document.querySelector("#cs-phone");
    const statusElement     = document.querySelector("#status-call");
    // button object selector
    const callButton        = document.querySelector(".btn-call");
    callButton.setAttribute('disabled', 'disabled');
    const hangupButton      = document.querySelector(".btn-hangup");
    const closeButton       = document.querySelector('.btn-close');

    closeButton.style.removeProperty('display');
    const statusButton      = document.querySelector('.btn-status');
    // input object selector
    let campaign_result     = document.querySelector('#campaignresult');
    let note_contacted      = document.querySelector('#note_contacted');
    let note_container      = document.querySelector('.note_container');
    let nameContainer       = document.querySelector('.name-container');
    let campaignContainer   = document.querySelector('.campaign-container');
    let waContainer         = document.querySelector('.wa-container');
    let inputCallStatus     = document.querySelector('#call-status');
    // call status check
    let callStatusCompleted = false;
    let callStatusAnswered  = false;
    // reset property
    campaign_result.value   = "";
    note_container.style    = 'display: none';
    nameContainer.style     = 'display: none';

    // sound object
    var sndAnswered = new Audio("{{ asset('storage/sounds/answered.wav') }}");

    // listening to event
    campaign_result.addEventListener('change', function(){
      if(campaign_result.value != ''){
        note_container.style.removeProperty('display');
        note_contacted.setAttribute('required', 'required');
      }else{
        note_container.style = 'display: none';
        note_contacted.removeAttribute('required');
      }
    });
    // nexmo status reset
    statusElement.innerText = '';
    inputCallStatus.value = '';
    // nexmo call button reset
    callButton.style.display = "inline";
    hangupButton.style.display = "none";
    // timeouts set
    setTimeout(() => {
      callButton.removeAttribute('disabled');
    }, 5000);
    // nexmo object start
    new NexmoClient({ debug: true }).login(USER_JWT).then(app => {
      callButton.addEventListener("click", event => {
        event.preventDefault();
        let number = String(target_number);
        console.log(number);
        if (number !== ""){
          app.callServer(number).catch(function(error){
            console.log('debug: ',error);
          });
        } else {
          statusElement.innerText = 'Please enter your phone number.';
        }
      });
      app.on("member:call", (member, call) => {
        // object selector reset
        callButton.style.display = 'none';
        closeButton.style.display = 'none';
        hangupButton.style.removeProperty('display');
        statusButton.style.removeProperty('display');
        $('#wa-valid').removeAttr('checked');
        // event when hangup button clicked
        hangupButton.addEventListener("click", () => {
          call.hangUp();
        });
      });
      app.on("call:status:changed",(call) => {
        console.log('Periodik : ', call.status);
        // animation call
        let statusAnimation = `<p class="saving">Call status: ${call.status}<span>.</span><span>.</span><span>.</span></p>`;
        // assign call animation to nexmo status display
        statusElement.innerHTML = statusAnimation;
        // filter nexmo status condition
        switch(call.status) {
          case call.CALL_STATUS.STARTED:
            console.log('Case call status: ', call.status);
            break;
          case call.CALL_STATUS.RINGING:
            console.log('Case call status: ', call.status);
            break;
          case call.CALL_STATUS.FAILED:
            inputCallStatus.value = call.status;
            callStatusAnswered = false;
            callButton.style.display = 'none';
            hangupButton.style.display = 'none';
            statusButton.style.removeProperty('display');
            updateButton.style.removeProperty('display');
            trashButton.style.removeProperty('display');
            waContainer.style.removeProperty('display');
            closeButton.style.removeProperty('dispaly');
            nameContainer.style.removeProperty('display');
            console.log('Case call status: ', call.status);
            break;
          case call.CALL_STATUS.CANCELLED:
            inputCallStatus.value = call.status;
            callStatusAnswered = false;
            callButton.style.removeProperty('display');
            hangupButton.style.display = 'none';
            statusButton.style.display = 'none';
            nameContainer.style.removeProperty('display');
            updateButton.style.removeProperty('display');
            trashButton.style.removeProperty('display');
            waContainer.style.removeProperty('display');
            closeButton.style.removeProperty('dispaly');
            console.log('Case call status: ', call.status);
            break;
          case call.CALL_STATUS.COMPLETED:
            callStatusCompleted = true;
            callButton.style.display = 'none';
            hangupButton.style.display = 'none';
            updateButton.style.removeProperty('display');
            closeButton.style.display = 'none';
            statusButton.style.display = 'none';
            campaign_result.setAttribute('required', 'required');
            nameContainer.style.removeProperty('display');
            campaignContainer.style.removeProperty('display');
            dataTableAdminLeader.ajax.reload();
            console.log('Case call status: ', call.status);
            break;
          case call.CALL_STATUS.ANSWERED:
            // play sound
            sndAnswered.play();
            inputCallStatus.value = call.status;
            callStatusAnswered = true;
            callButton.style.display = 'none';
            hangupButton.style.removeProperty('display');
            nameContainer.style.removeProperty('display');
            closeButton.style.display = 'none';
            statusButton.style.display = 'none';
            console.log('Case call status: ', call.status);
            break;
          default:
            // BUSY
            // REJECTED
            // TIMEOUT
            // UNANSWERED
            inputCallStatus.value = call.status;
            callStatusAnswered = false;
            callButton.style.display = 'none';
            hangupButton.style.display = 'none';
            updateButton.style.removeProperty('display');
            trashButton.style.removeProperty('display');
            statusButton.style.display = 'none';
            nameContainer.style.removeProperty('display');
            waContainer.style.removeProperty('display');
            closeButton.style.removeProperty('dispaly');
            console.log('Case call status: ', call.status);
            console.log('Case call status default: ', call.status);
            break;
        }
      });
    }).catch(function(){
      alert('Network Problem, refresh page and try again later. Please contact dev team if this problem not fix in few minutes.');
      console.error;
      $('#phone-call-modal').modal('hide');
    });
  });

Solution

  • There is no possible way for the Vonage (Nexmo) Voice API to validate whether your user should be able to call a phone number they have access to in your application. It sounds like you have provided users of your application the ability to initiate voice conversations with your Vonage API credentials. The validation logic for your users must rest in your application.

    One solution you can consider is before initiating the new voice call, you can check against your database if the user has access or not. If they do not have access you can redirect them to another view in your application, and if they do have access you can initiate the call. Laravel has a entire suite of validation tooling that you may find helpful.