📜 ⬆️ ⬇️

Service managed audio conferencing do-it-yourself

Audio conferencing is a convenient tool for solving a number of business problems, most are accustomed to using something ready, for example, Skype. But there are a number of cases when a company needs its own centrally managed instrument so that a secretary or coordinator can create a conference, bring people into it and manage this process. We will consider just such a case and will use the VoxImplant cloud platform to implement the necessary functionality, as well as other useful web libraries à la bootstrap and jquery.
To begin with, we will define the minimum necessary functionality of our conference service. We need the conference administrator to create a list of conference participants, specify the connection type for the participant through the incoming access number + pin, connect outgoing calls from the conference server (for some, it may be more convenient), as an additional option - a call via the web application made using the Web SDK. The real-time conference administrator should be able to observe which participants are connected, mute / unmute participants, disconnect and connect conference participants on the fly. Accordingly, the prototype of the administrator interface will be as follows.

Conference Settings



Everything is simple: the choice of access numbers to the conference, you can / can not connect to the conference without authorization, try to log in by the caller in the conference. The choice of the number is done through a request to the HTTP API VoxImplant, we get a list of numbers connected to the conference application (more on this later), the rest is just the settings that we save in the database in the conferences table.

Conference participants



Familiar to all bootstrap to help us, the logic of the interface and its interaction with a simple web service working with the database from 3 tables (managers, conferences, participants) we wrote on Reactjs. We will not go into the details of the development of interfaces now, the post is not a little bit about that, if there are suggestions - we are always happy to tell you more, but this requires a separate article. At the end, there will be links to the SQL script for table deployment, a PHP script with web services, and the bundled client part of the service, we will put all this stuff on GitHub.
With the addition of participants, everything is also not particularly difficult - there is a name of the participant, a phone number (for outgoing connection or if A-number authorization is enabled), email (for sending notifications of participation in the conference and data for connecting to it), Outbound - connect outgoing call after the start of the conference. When the list of participants is formed (all data is recorded in the database), you can start the conference. Schedule Conference allows you to schedule a conference for some date in the future, the difference with Start Conference is obvious - we simply write data to the database, including the data when the conference is relevant and only then can it be connected from the outside + it will have to create during the creation itself participants whose Outbound == true.
')

VoxImplant Conferences


Before you start writing scripts VoxImplant for conferences need to understand how they work. Conferences in VoxImplant come in 2 types: some can be created directly in a script using the createConference function and only calls that were created during the current session can be connected to them, the second ones are created on special servers by a special HTTP API command or after a call is redirected to these servers using special rules and functions callConference . We need the second option to implement the service. In fact, there will be 2 main scenarios - the gatekeeper and the conference, the gatekeeper will be responsible for authorizing incoming calls and redirect them to the conference upon successful authorization. In the main scenario, we will collect all calls and process external commands that the session with the conference will receive via the HTTP interface.

Let's start with a simple, gatekeeper script will look like this:
var call, //   input = '', //      WEBSERVICE_URL = "path/to/shim.php", INTRO_MUSIC_URL = "path/to/music.mp3", t1, sessionCookie = null; // id     - (shim.php)    /** *    */ VoxEngine.addEventListener(AppEvents.CallAlerting, function (e) { call = e.call; //      call.startEarlyMedia(); call.startPlayback(INTRO_MUSIC_URL, true); //    call.addEventListener(CallEvents.Connected, handleCallConnected); call.addEventListener(CallEvents.Disconnected, function (e) { VoxEngine.terminate(); }); call.addEventListener(CallEvents.Failed, function (e) { VoxEngine.terminate(); }); call.addEventListener(CallEvents.ToneReceived, handleToneReceived); //   - (shim.php) var opts = new Net.HttpRequestOptions(); opts.method = "GET"; opts.headers = ["User-Agent: VoxImplant"]; //      (  ) var authInfo = { username: "username", password: "password" }; Net.httpRequest(WEBSERVICE_URL + '?action=authorize&params=' + encodeURIComponent(JSON.stringify(authInfo)), authResult, opts); }); function authResult(e) { // HTTP 200 ,   if (e.code == 200) { //  ID-  HTTP response  for (var i in e.headers) { if (e.headers[i].key == "Set-Cookie") { sessionCookie = e.headers[i].value; sessionCookie = sessionCookie.substr(0, sessionCookie.indexOf(';')); } } // -   ,   ( -    ) if (sessionCookie == null) { Logger.write("No session header found."); VoxEngine.terminate(); } Logger.write("Auth Result: " + e.text + " Session Cookie: " + sessionCookie); //   ,  ship.php     AUTHORIZED if (JSON.parse(e.text).result == "AUTHORIZED") { //     () call.handleTones(true); //    -    CallEvents.CallConnected call.answer(); } else { Logger.write("Authorization failed"); VoxEngine.terminate(); } } else { Logger.write("Auth HTTP request failed: " + e.code); VoxEngine.terminate(); } } /** *      */ function handleCallConnected(e) { //   call.stopPlayback(); //  ,  TTS        (. CallEvents.ToneReceived) call.say("Hello! Welcome to VoxImplant conferencing, please enter your conference access code, " + "followed by the pound sign", Language.UK_ENGLISH_FEMALE); //     TTS call.addEventListener(CallEvents.PlaybackFinished, handleIntroPlayed); } /** *     TTS */ function handleIntroPlayed(e) { //    listener call.removeEventListener(CallEvents.PlaybackFinished, handleIntroPlayed); //  5         t1 = setTimeout(function () { call.say("Please enter your conference access code, " + "followed by the pound sign", Language.UK_ENGLISH_FEMALE); call.addEventListener(CallEvents.PlaybackFinished, handleIntroPlayed); }, 5000); } /** *      */ function handleToneReceived(e) { clearTimeout(t1); call.removeEventListener(CallEvents.PlaybackFinished, handleIntroPlayed); call.stopPlayback(); //   ,    if (e.tone == '#') { var opts = new Net.HttpRequestOptions(); opts.method = "GET"; opts.headers = ["Cookie: " + sessionCookie]; //   - -       var requestInfo = { access_number: call.number().replace(/[\s\+]+/g, ''), access_code: input }; Net.httpRequest(WEBSERVICE_URL + "?action=get_conference&params=" + encodeURIComponent(JSON.stringify(requestInfo)), getConferenceResult, opts); } else input += e.tone; } /** *    get_conference */ function getConferenceResult(e) { if (e.code == 200) { var result = JSON.parse(e.text); if (typeof result.result != "undefined") { //  id  result = result.result; //             call.removeEventListener(CallEvents.ToneReceived, handleToneReceived); call.handleTones(false); input = ''; Logger.write('Joining conference conf' + result.conference_id); //       conf + id-   callerid var conf = VoxEngine.callConference('conf' + result.conference_id, call.callerid()); VoxEngine.sendMediaBetween(call, conf); } else { input = ''; call.say("Sorry, there is no conference with entered access code, please try again.", Language.UK_ENGLISH_FEMALE); call.addEventListener(CallEvents.PlaybackFinished, handleIntroPlayed); } } else { Logger.write("GetConference HTTP request failed: " + e.code); input = ''; call.say("Sorry, there is no conference with entered access code, please try again.", Language.UK_ENGLISH_FEMALE); call.addEventListener(CallEvents.PlaybackFinished, handleIntroPlayed); } } 


Save the script, you can call it, for example, ConferenceGatekeeper. To associate a script with a number, you need to purchase a number in the VoxImplant control panel, then create an application in the Applications section, you can call it conference (the full name will be conference.youracccountname.voximplant.com). For the binding, we create an application rule (Rule) in the Rules section, call it IncomingCall, in which we write the purchased phone number to the Pattern, and in Assigned, drag and drop our previously created ConferenceGatekeeper script.



We save the application and in the section with the purchased numbers we attach the number to the application.



If everything is done correctly, then the incoming call to the number will launch our script. You can proceed to the scenario of the conference itself, but before that you need to take into account one important feature of the VoxImplant architecture - a session without active calls lives for exactly 1 minute, after which it ends, this applies equally to sessions with a conference. If we start a conference via an HTTP StartConference request (for example, to connect some of the participants with outgoing calls), then media_session_access_url, which returns this request will not be relevant forever, but exactly until the session is terminated. Similarly, if a conference session is started via incoming calls, we will therefore keep media_session_access_url in the database and delete it when the session is over and overwrite it if the session is restarted, so that we have either the current URL or it is not there at all.

So, create a new script of the following form:

 var voxConf, //  VoxConference (      ) conferenceId = null, // id  startType, //    eventType, //      redial_pId, // id   authorized = false, //   WEBSERVICE_URL = "path/to/shim.php", //   - sessionCookie = null, //   id     - t3, ms_url; // media_session_access_url /** *    */ VoxEngine.addEventListener(AppEvents.Started, function (e) { //    VoxConference voxConf = new VoxConference(); //  media_session_access_url ms_url = e.accessURL; //     HTTP,  customData ,   ,   try { data = JSON.parse(VoxEngine.customData()); conferenceId = data.conference_id; startType = data.start_type; if (typeof data.event != 'undefined') eventType = data.event; if (typeof data.pId != 'undefined') redial_pId = data.pId; } catch (e) { startType = "sip"; } //   - (shim.php) var opts = new Net.HttpRequestOptions(); opts.method = "GET"; opts.headers = ["User-Agent: VoxImplant"]; //      (  ) var authInfo = { username: "username", password: "password" }; Net.httpRequest(WEBSERVICE_URL + "?action=authorize&params=" + encodeURIComponent(JSON.stringify(authInfo)), authResult, opts); }); /** *    */ VoxEngine.addEventListener(AppEvents.CallAlerting, function (e) { //         -,          waiting if (voxConf.participants() == null) { // e.destination       conf + id,  id conferenceId = e.destination.replace('conf', ''); voxConf.addConfCall({ call_id: e.call.id(), call: e.call, input: '', state: 'waiting' }); } else { //      ,      voxConf.addConfCall({ call_id: e.call.id(), call: e.call, input: '', state: 'in_process' }); voxConf.processIncomingCall(e.call); } }); /** *     */ function authResult(e) { if (e.code == 200) { for (var i in e.headers) { if (e.headers[i].key == "Set-Cookie") { sessionCookie = e.headers[i].value; sessionCookie = sessionCookie.substr(0, sessionCookie.indexOf(';')); } } if (sessionCookie == null) { Logger.write("No session header found."); VoxEngine.terminate(); } if (JSON.parse(e.text).result == "AUTHORIZED") { //    authorized = true; //      HTTP    conferenceId if (startType == 'http' || conferenceId != null) { //     -  media_session_access_url   if (startType == 'sip') saveMSURL(); //      getParticipants(); } else { //    conferenceId t3 = setInterval(checkConferenceId, 1000); } } else { Logger.write("Authorization failed"); VoxEngine.terminate(); } } else { Logger.write("Auth HTTP request failed: " + e.code); VoxEngine.terminate(); } } /** * ,     */ function processRedial(pId) { var phone = '', participants = voxConf.participants(); //    id = pId for (var k in participants) { if (participants[k].id == pId) { phone = participants[k].phone; } } //   if (phone == '') return false; //  -      var call = VoxEngine.callPSTN(phone, voxConf.getConfNumber()); voxConf.addConfCall({ call_id: call.id(), call: call, input: '', state: 'in_process', participant_id: pId }); voxConf.processOutboundCall(call); return true; } /** *  conferenceId,  ,        */ function checkConferenceId() { if (conferenceId != null) { clearInterval(t3); //  media_session_access_url   if (startType == 'sip') saveMSURL(); //      getParticipants(); } } /** *  media_session_access_url    - */ function saveMSURL() { var opts = new Net.HttpRequestOptions(); opts.method = "POST"; opts.headers = ["Cookie: " + sessionCookie]; var requestInfo = { conference_id: conferenceId, ms_url: ms_url }; Net.httpRequest(WEBSERVICE_URL + "?action=save_ms_url&params=" + encodeURIComponent(JSON.stringify(requestInfo)), saveMSURLresult, opts); } //    save_ms_url function saveMSURLresult(e) { if (e.code == 200) { //  . TODO:    Logger.write(e.text); } else { //  . TODO:    Logger("Couldn't save ms_url in DB"); } } /** *       - */ function getParticipants() { var opts = new Net.HttpRequestOptions(); opts.method = "GET"; opts.headers = ["Cookie: " + sessionCookie]; var requestInfo = { conference_id: conferenceId }; Net.httpRequest(WEBSERVICE_URL + "?action=get_participants&params=" + encodeURIComponent(JSON.stringify(requestInfo)), getParticipantsResult, opts); } /** *    get_participants */ function getParticipantsResult(e) { if (e.code == 200) { if (typeof e.text == 'undefined') { // -   . TODO:    Logger.write('No participants found.'); VoxEngine.terminate(); return; } var result = JSON.parse(e.text); if (typeof result.error != 'undefined') { // -   . TODO:    } else { //      var participants = result.result.participants, calleridAuth = (result.result.conference.callerid_auth == "1"), anonymousAccess = (result.result.conference.anonymous_access == "1"), accessCode = result.result.conference.access_code, active = (result.result.conference.active == "1"), accessNumber = result.result.conference.access_number; //  voxConf.init(conferenceId, accessCode, anonymousAccess, calleridAuth, active, participants, accessNumber); //  ,         - voxConf.processWaitingCalls(); //     HTTP -       if (startType == "http") { if (eventType == 'redial') voxConf.makeOutboundCalls(redial_pId); //          else voxConf.makeOutboundCalls(); //       } } } else Logger.write("Participants HTTP request failed: " + e.code); } /** *          */ function updateParticipants(e, pId) { if (e.code == 200) { if (typeof e.text == 'undefined') return; // -   . TODO:    var result = JSON.parse(e.text); if (typeof result.error != 'undefined') { // -   . TODO:    } else { //    voxConf.updateParticipants(result.result.participants); //    processRedial(pId); } } } /** *      media_session_access_url */ VoxEngine.addEventListener(AppEvents.HttpRequest, function (data) { //    media_session_access_url + /command=mute_participant/pId=100 //  data.path   command=mute_participant/pId=100 var params = data.path.split("/"), command = null, pId = null, options = null; //  params    command, pId, options for (var i in params) { var kv = params[i].split("="); if (kv.length > 1) { if (kv[0] == "command") command = kv[1]; if (kv[0] == "pId") pId = kv[1]; if (kv[0] == "options") options = kv[1]; } } //       var calls = voxConf.calls(); switch (command) { //        -      - case "gather_info": var result = []; for (var i in calls) { if (typeof calls[i].participant_id != 'undefined') result.push({ state: calls[i].call.state(), participant_id: calls[i].participant_id }); } return JSON.stringify(result); break; //   case "disconnect_participant": for (var i in calls) { if (calls[i].participant_id == pId) { calls[i].call.hangup(); return true; } } return false; break; //        case "mute_participant": for (var i in calls) { if (calls[i].participant_id == pId) { calls[i].call.stopMediaTo(voxConf.getConfObj()); return true; } } return false; break; //        case "unmute_participant": for (var i in calls) { if (calls[i].participant_id == pId) { calls[i].call.sendMediaTo(voxConf.getConfObj()); return true; } } return false; break; //     case "redial_participant": //          - if (options == 'reload_participants') { var opts = new Net.HttpRequestOptions(); opts.method = "GET"; opts.headers = ["Cookie: " + sessionCookie]; var requestInfo = { conference_id: conferenceId }; Net.httpRequest(WEBSERVICE_URL + "?action=get_participants&params=" + encodeURIComponent(JSON.stringify(requestInfo)), function(e) { //    updateParticipants(e, pId); }, opts); return true; } else { //       return processRedial(pId); } break; } }); /** *       -   media_session_access_url (ms_url)   */ VoxEngine.addEventListener(AppEvents.Terminating, function(e) { var opts = new Net.HttpRequestOptions(); opts.method = "POST"; opts.headers = ["Cookie: " + sessionCookie]; Logger.write("HEADERS: " + opts.headers); var requestInfo = { conference_id: conferenceId, ms_url: '' }; Logger.write("Terminating the session, update ms_url in database"); Net.httpRequest(WEBSERVICE_URL + "?action=save_ms_url&params=" + encodeURIComponent(JSON.stringify(requestInfo)), function(e) {}, opts); }); 


Save the script with the name StandaloneConference. You probably noticed that we used a certain VoxConference class in this scenario, it is not a non-built-in class, it’s just a separate class responsible for the conference, which we wrote in a separate script and will hook it to the rule in front of our StandaloneConference. Let's take a closer look at it:

If you have read this far, then it means you are really interested, there is still a little more :)

 // Magic!   VoxImplant require(Modules.Conference); //    - require(Modules.Player); //     /** *    VoxConference,       */ VoxConference = function () { var conferenceId, accessCode, anonymousAccess, calleridAuth, active, participants = null, number, conf, calls = [], t1, t2, music = null, BEEP_URL = "path/to/beep.mp3", MUSIC_URL = "path/to/ambientmusic.mp3"; //    this.init = function (id, code, a_access, c_auth, a, p, num) { conferenceId = id; // id  number = num; //    accessCode = code; //   anonymousAccess = a_access; //     calleridAuth = c_auth; //      participants = p; //   active = a; //   conf = VoxEngine.createConference(); } //    this.participants = function () { return participants; } //    this.updateParticipants = function (newdata) { participants = newdata; } //     this.calls = function () { return calls; } //    this.getConfNumber = function () { return number; } //    this.getConfObj = function () { return conf; } //     this.processIncomingCall = function (call) { //    call.answer(); //   this.handleCallConnected = this.handleCallConnected.bind(this); call.addEventListener(CallEvents.Connected, this.handleCallConnected); call.addEventListener(CallEvents.Disconnected, function (e) { //      calls      participants var pid = this.getConfCall(e.call).participant_id; Logger.write("Participant id " + pid + " has left the conf"); this.participantLeft(pid); for (var i = 0; i < calls.length; i++) { if (calls[i].call == e.call) { calls.splice(i, 1); break; } } //     ,     this.ambientMusic(); }.bind(this)); } //     this.processOutboundCall = function (call) { //    call.answer(); //   this.handleOutboundCallConnected = this.handleOutboundCallConnected.bind(this); call.addEventListener(CallEvents.Connected, this.handleOutboundCallConnected); call.addEventListener(CallEvents.Disconnected, function (e) { //      calls      participants var pid = this.getConfCall(e.call).participant_id; Logger.write("Participant id " + pid + " has left the conf"); this.participantLeft(pid); for (var i = 0; i < calls.length; i++) { if (calls[i].call == e.call) { calls.splice(i, 1); break; } } this.ambientMusic(); }.bind(this)); call.addEventListener(CallEvents.Failed, function (e) { //    var pid = this.getConfCall(e.call).participant_id; Logger.write("Couldnt connect participant id " + pid); }.bind(this)); } //       ,      this.participantExists = function (passcode) { Logger.write("Check if participant exists, passcode: " + passcode); for (var i = 0; i < participants.length; i++) { if (participants[i].passcode == passcode && participants[i].connected != true) { participants[i].connected = true; return participants[i].id; } } return false; } //    ,     ,        this.participantWithParamExists = function (param, value) { Logger.write("Check if with participant." + param + " = " + value + " exists"); for (var i = 0; i < participants.length; i++) { if (participants[i][param] == value && participants[i].connected != true) { participants[i].connected = true; return participants[i].id; } } return false; } //     connected = false this.participantLeft = function (id) { for (var i = 0; i < participants.length; i++) { if (participants[i].id == id) { participants[i].connected = false; } } } //     ,       this.processWaitingCalls = function () { for (var i = 0; i < calls.length; i++) { if (calls[i].state == 'waiting') { calls[i].state = 'in_process'; this.processIncomingCall(calls[i].call); } } } //    ,       (auto_call) this.makeOutboundCalls = function (pId) { for (var i in participants) { if ((participants[i].auto_call == "1" && typeof pId == 'undefined') || pId == participants[i].id) { var call = VoxEngine.callPSTN(participants[i].phone, number); this.addConfCall({ call_id: call.id(), call: call, input: '', state: 'in_process', participant_id: participants[i].id }); Logger.write(JSON.stringify(calls)); this.processOutboundCall(call); } } } //     calls this.getConfCall = function (call) { for (var i in calls) { if (calls[i].call == call) return calls[i]; } } //     calls this.addConfCall = function (call_obj) { calls.push(call_obj); } //       this.handleOutboundCallConnected = function (e) { //     calls var cCall = this.getConfCall(e.call); cCall.state = 'connected'; //       (  ) VoxEngine.sendMediaBetween(e.call, conf); //           var snd = VoxEngine.createURLPlayer(BEEP_URL); snd.sendMediaTo(conf); snd.addEventListener(PlayerEvents.PlaybackFinished, function (ee) { //       -   this.ambientMusic(); }.bind(this)); } //        (  ConferenceGatekeeper) this.handleCallConnected = function (e) { //   e.call.stopPlayback(); //       (DTMF) e.call.handleTones(true); //      -    (passcode) this.authStep2(e.call); } //          ,     this.handleIntroPlayedStage2 = function (e) { e.call.removeEventListener(CallEvents.PlaybackFinished, this.handleIntroPlayedStage2); t2 = setTimeout(function () { e.call.say("Please specify your passcode, followed by " + "the pound sign to join the conference.", Language.UK_ENGLISH_FEMALE); e.call.addEventListener(CallEvents.PlaybackFinished, this.handleIntroPlayedStage2); }.bind(this), 5000); } //     this.handleToneReceivedStage2 = function (e) { clearTimeout(t2); e.call.removeEventListener(CallEvents.PlaybackFinished, this.handleIntroPlayedStage2); e.call.stopPlayback(); var cCall = this.getConfCall(e.call); //    #   if (e.tone == "#") { Logger.write("Checking passcode: " + cCall.input); participant_id = this.participantExists(cCall.input); if (participant_id != false) { //     cCall.input = ""; cCall.state = "connected"; cCall.participant_id = participant_id; e.call.removeEventListener(CallEvents.ToneReceived, this.handleToneReceivedStage2); Logger.write("Participant id " + participant_id + " has joined the conf"); //    this.joinConf(e.call); } else { //    -       cCall.input = ""; e.call.say("Sorry, wrong passcode was specified, please try again.", Language.UK_ENGLISH_FEMALE); e.call.addEventListener(CallEvents.PlaybackFinished, this.handleIntroPlayedStage2); } } else cCall.input += e.tone; //    #     input } //        this.joinConf = function (call) { call.say("You have joined the conference.", Language.UK_ENGLISH_FEMALE); call.addEventListener(CallEvents.PlaybackFinished, function (e) { //       (  ) VoxEngine.sendMediaBetween(call, conf); //           var snd = VoxEngine.createURLPlayer(BEEP_URL); snd.sendMediaTo(conf); snd.addEventListener(PlayerEvents.PlaybackFinished, function (ee) { //       -   this.ambientMusic(); }.bind(this)); }.bind(this)); } //    /        this.ambientMusic = function () { var p_num = 0; for (var i in calls) { if (calls[i].state == 'connected') p_num++; } if (p_num == 1) { // 1  -   music = VoxEngine.createURLPlayer(MUSIC_URL, true); music.sendMediaTo(conf); } else { // 2+  -   music.stopMediaTo(conf); } } //     (passcode) this.passcodeCheck = function (call) { call.say("Thank you! Please specify your passcode, followed by " + "the pound sign to join the conference.", Language.UK_ENGLISH_FEMALE); this.handleToneReceivedStage2 = this.handleToneReceivedStage2.bind(this); call.addEventListener(CallEvents.ToneReceived, this.handleToneReceivedStage2); this.handleIntroPlayedStage2 = this.handleIntroPlayedStage2.bind(this); call.addEventListener(CallEvents.PlaybackFinished, this.handleIntroPlayedStage2); } //    -         this.authStep2 = function (call) { if (anonymousAccess) { //   -     this.joinConf(call); this.getConfCall(call).participant_id = null; } else { if (calleridAuth) { //      ,   callerid_auth var participant_id = this.participantWithParamExists("phone", call.callerid()); if (participant_id != false) { //   -     this.joinConf(call); this.getConfCall(call).participant_id = participant_id; } else { //      -      (passcode) this.passcodeCheck(call); } } else { //    (passcode) this.passcodeCheck(call); } } } }; 


So, now we have 2 more scenarios - StandaloneConference and VoxConference, in order to connect them to call processing, we need to create some additional rules for our application. Let's call them FwdToConf - Pattern will be conf, and StartConfHTTP - Pattern will do. *, In both cases we drag VoxConference and StandaloneConference to Assigned:


We connect the number to the application in the section My Phone Numbers



All save and proceed to the deployment of the database and web service. We have drilled the service in PHP, for the database we took MySQL. In addition to deploying the service, you will need a web client to manage the entire farm - if you don’t want to dig into the database all the time, we quickly did it on ReactJS and Bootstrap. There is a true one nuance - you will have to configure the web server in a certain way, since we use the AJAX + session on the + crossDomain server, otherwise the browser will swear.

For Nginx, setting up our cross-domain AJAX will look like this:
 add_header 'Access-Control-Allow-Origin' $http_origin; add_header 'Access-Control-Allow-Credentials' 'true'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; 


All other files, including the database structure for mysql and the user admin / admin on behalf of whom you can immediately use the functions of conferences, as well as a ready-made web application can be taken from our GitHub account:

The project with all the files on GitHub

Source: https://habr.com/ru/post/229593/


All Articles