diff --git a/README.md b/README.md index 0f1fc2b..4d897e2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # daster -Управление диалпланом +Dialplan Asterisk - управление диалпланом + +![mainpage](images/mainpage.png) diff --git a/database/script.sql b/database/script.sql index 4789ce4..beecc0f 100644 --- a/database/script.sql +++ b/database/script.sql @@ -1,38 +1,38 @@ create table if not exists da_groups ( - da_name varchar(20) not null, - da_comment varchar(100) default null, - constraint da_groups_pk primary key (da_name) + da_name varchar(20) not null, + da_comment varchar(100) default null, + constraint da_groups_pk primary key (da_name) ); insert into da_groups (da_name, da_comment) - values - ('general', 'Общие контакты'), - ('work', 'Рабочие контакты'), - ('personal', 'Личные контакты'); + values + ('general', 'Общие контакты'), + ('work', 'Рабочие контакты'), + ('personal', 'Личные контакты'); create table if not exists da_lists ( - da_name varchar(20) not null, - da_comment varchar(100) default null, - constraint da_lists_pk primary key (da_name) + da_name varchar(20) not null, + da_comment varchar(100) default null, + constraint da_lists_pk primary key (da_name) ); insert into da_lists (da_name, da_comment) - values - ('general', 'Общий'), - ('whitelist', 'Белый'), - ('blacklist', 'Черный'); + values + ('general', 'Общий'), + ('whitelist', 'Белый'), + ('blacklist', 'Черный'); create table if not exists da_numbers ( - da_number varchar(12) not null, - da_group varchar(20) not null default 'general', - da_list varchar(20) not null default 'general', - da_all_cc int not null default 0, - da_white_cc int not null default 0, - da_black_cc int not null default 0, - da_comment varchar(100) default null, - constraint da_numbers_pk primary key (da_number), - foreign key (da_group) references da_groups (da_name) on delete set null on update cascade, - foreign key (da_list) references da_lists (da_name) on delete set null on update cascade + da_number varchar(12) not null, + da_group varchar(20) not null default 'general', + da_list varchar(20) not null default 'general', + da_all_cc int not null default 0, + da_white_cc int not null default 0, + da_black_cc int not null default 0, + da_comment varchar(100) default null, + constraint da_numbers_pk primary key (da_number), + foreign key (da_group) references da_groups (da_name) on delete set null on update cascade, + foreign key (da_list) references da_lists (da_name) on delete set null on update cascade ); create table if not exists da_sms ( @@ -44,6 +44,21 @@ create table if not exists da_sms ( constraint da_sms_pk primary key (da_id) ); +create table if not exists da_ussd_type ( + da_id bigserial not null, + da_comment varchar(100) not null, + constraint da_ussd_type_pk primary key (da_id) +); + +insert into da_ussd_type (da_id, da_comment) + values + ('0', 'Уведомление'), + ('1', 'Запрос'), + ('2', 'Прервано сетью'), + ('3', 'Ответ другого локального клиента'), + ('4', 'Операция не поддерживается'), + ('5', 'Тайм-аут сети'); + create table if not exists da_ussd ( da_id bigserial not null, da_date timestamp not null default NOW(), @@ -54,9 +69,10 @@ create table if not exists da_ussd ( ); create table if not exists da_server ( - da_address varchar(50) not null, + da_id int not null default 1, da_transparent_mode bool not null default false, da_internal_number varchar(12) not null, da_external_number varchar(12) not null, - constraint da_server_pk primary key (da_address) + da_external_number_on bool not null default true, + constraint da_server_pk primary key (da_id) ); diff --git a/dub.json b/dub.json index 5814e7b..d1e1943 100644 --- a/dub.json +++ b/dub.json @@ -5,10 +5,9 @@ "copyright": "Copyright © 2023, Alexander Zhirov", "dependencies": { "vibe-d": "~>0.9", - "ldap": "~>0.4", - "singlog": "~>0.3.1", + "singlog": "~>0.4.0", "arsd-official:postgres": "~>10.9.10", - "readconf": "~>0.3.1" + "readconf": "~>0.4.0" }, "buildTypes": { "debug": { @@ -26,7 +25,7 @@ } }, "description": "Dialplan Asterisk - веб-сервер для управления обработкой вызовов Asterisk", - "license": "proprietary", + "license": "GPL-2.0", "name": "daster", "targetPath": "bin", "targetType": "executable" diff --git a/dub.selections.json b/dub.selections.json index f10278b..08bea5a 100644 --- a/dub.selections.json +++ b/dub.selections.json @@ -5,15 +5,14 @@ "datefmt": "1.0.4", "diet-ng": "1.8.1", "eventcore": "0.9.25", - "ldap": "0.4.0", "libasync": "0.8.6", "memutils": "1.0.9", "mir-linux-kernel": "1.0.1", "openssl": "3.3.0", "openssl-static": "1.0.2+3.0.8", - "readconf": "0.3.1", - "silly": "1.1.1", - "singlog": "0.3.1", + "readconf": "0.4.0", + "silly": "1.2.0-dev.2", + "singlog": "0.4.0", "stdx-allocator": "2.77.5", "taggedalgebraic": "0.11.22", "vibe-core": "2.2.0", diff --git a/images/404.png b/images/404.png new file mode 100644 index 0000000..116c3e7 Binary files /dev/null and b/images/404.png differ diff --git a/images/favicon.png b/images/favicon.png index f78f532..e220943 100644 Binary files a/images/favicon.png and b/images/favicon.png differ diff --git a/images/mainpage.png b/images/mainpage.png new file mode 100644 index 0000000..d5cf9bf Binary files /dev/null and b/images/mainpage.png differ diff --git a/js/authorization.js b/js/authorization.js new file mode 100644 index 0000000..4edab3f --- /dev/null +++ b/js/authorization.js @@ -0,0 +1,48 @@ +$(document).ready(function () { + noticer = new Noticer; + + $("#authorization").button({ icon: "ui-icon-home" }); + + $("#login, #password").on('keypress',function(e) { + if(e.which == 13) { + authorization() + } + }); + + $("#authorization").click(() => { + authorization() + }); + + $("body").fadeTo(500, 1); +}); + +function authorization() { + request().then((data) => { + data.error ? noticer.error(data.message) : (window.location.href = "."); + }).catch((e) => { + noticer.error(e.message); + }); +} + +async function request() { + let login = $("#login").val(); + let password = $("#password").val(); + + let response = await fetch('.', { + method: 'POST', + headers: { + 'Content-Type': 'application/json;charset=utf-8' + }, + body: JSON.stringify({ + login: login, + password: password, + query: "login" + }) + }); + + if (!response.ok) + throw new Error(`Произошла неизвестаня ошибка: ${response.status}`); + + const data = await response.json(); + return data; +} diff --git a/js/message.js b/js/message.js deleted file mode 100644 index e064ac5..0000000 --- a/js/message.js +++ /dev/null @@ -1,100 +0,0 @@ -class Message { - timer; - - constructor() { - this.div = $('
'); - this.div.css({ - "position": "absolute", - "top": "10px", - "right": "20px", - "z-index": "1000" - }); - - $('body').append(this.div); - } - - success(message, delay = 6000) { - this.print(message, delay, '#52b818', '#bffdc0'); - } - - warning(message, delay = 6000) { - this.print(message, delay, '#b8ae18', '#f8fdbf'); - } - - error(message, delay = 6000) { - this.print(message, delay, '#b96161', '#fddede'); - } - - print = function(message, delay, border, background) { - if (delay < 6000) delay = 6000; - let Timer = function(callback, delay) { - let timerId, start, remaining = delay; - - this.pause = function() { - clearTimeout(timerId); - remaining -= new Date() - start; - }; - - this.resume = function() { - start = new Date(); - clearTimeout(timerId); - timerId = setTimeout(callback, remaining); - }; - - this.dead = function() { - clearTimeout(timerId); - }; - - this.resume(); - } - - let newMessage = $(`
${message}
`); - - newMessage.css({ - "border": `1px solid ${border}`, - "background-color": `${background}`, - "color": "#333", - "padding": "10px 30px", - "text-align": "center", - "display": "none", - "margin": "10px 0 0 0", - "width": "350px", - "opacity": "1", - "cursor": "pointer" - }); - - newMessage.hover(function(){ - $(this).css({ - "opacity": "1" - }); - }, function(){ - $(this).css({ - "opacity": "0.3" - }); - }); - - this.div.append(newMessage); - - let opacityTimeout = setTimeout(function() { - newMessage.fadeTo(1000, 0.3); - }, 2500); - - newMessage.fadeIn(500).mouseenter(() => { - clearTimeout(opacityTimeout); - this.timer.pause(); - }).mouseleave(() => { - this.timer.resume(); - }).click(() => { - this.timer.dead(); - newMessage.fadeOut(0, function(){ - this.remove(); - }); - }); - - this.timer = new Timer(() => { - newMessage.fadeOut(500, function(){ - this.remove(); - }); - }, delay); - } -} diff --git a/js/noticer.min.js b/js/noticer.min.js new file mode 100644 index 0000000..e7604a1 --- /dev/null +++ b/js/noticer.min.js @@ -0,0 +1,4 @@ +/*! noticer - v0.1.1 - 2023-06-02 +* https://git.zhirov.kz/alexander/noticer +* Copyright Alexander Zhirov; Licensed GPL-2.0 */ +class Noticer{constructor(){this.div=$('
'),this.div.css({position:"absolute",top:"10px",right:"20px","z-index":"1000"}),$("body").append(this.div)}success(e,t=6e3){this.print(e,t,"#52b818","#bffdc0")}warning(e,t=6e3){this.print(e,t,"#b8ae18","#f8fdbf")}error(e,t=6e3){this.print(e,t,"#b96161","#fddede")}print=function(e,t,n,s){t<6e3&&(t=6e3);let a=function(e,t){let n,s,o=t;this.pause=function(){clearTimeout(n),o-=new Date-s},this.resume=function(){s=new Date,clearTimeout(n),n=setTimeout(e,o)},this.dead=function(){clearTimeout(n)},this.resume()},o=$(`
${e}
`);o.css({border:`1px solid ${n}`,"background-color":`${s}`,color:"#333",padding:"10px 30px","text-align":"center",display:"none",margin:"10px 0 0 0",width:"350px",opacity:"1",cursor:"pointer"}),o.hover(function(){$(this).css({opacity:"1"})},function(){$(this).css({opacity:"0.3"})}),this.div.append(o);let r=setTimeout(function(){o.fadeTo(1e3,.3)},2500);o.fadeIn(500).mouseenter(()=>{clearTimeout(r),i.pause()}).mouseleave(()=>{i.resume()}).click(()=>{i.dead(),o.fadeOut(0,function(){this.remove()})});let i=new a(()=>{o.fadeOut(500,function(){this.remove()})},t)}} diff --git a/js/script.js b/js/script.js index 2dc6f90..7a00ef9 100644 --- a/js/script.js +++ b/js/script.js @@ -1,27 +1,77 @@ -$(document).ready(function () { - message = new Message; +var numbers = []; +var sms = []; +var ussd = []; - // (new divNotFoundNumbers).push("Загрузка..."); +$(document).ready(function () { + noticer = new Noticer; + + let tabs = { + 0: () => { loadNumbers() }, + 1: () => { loadSMS() }, + 2: () => { loadUSSD() }, + 3: () => { loadServerInfo() } + }; + + let lists = { + 0: () => { showListNumbers($("#accordion-numbers .ui-accordion-content-active")) }, + 1: () => { showListSMS($("#accordion-sms .ui-accordion-content-active")) }, + 2: () => { showListUSSD($("#accordion-ussd .ui-accordion-content-active")) }, + 3: () => {} + }; + + let groups = { + 0: () => { generateListGroupNumbers($("#accordion-numbers .ui-accordion-content-active")) }, + 1: () => { generateListGroupSMS($("#accordion-sms .ui-accordion-content-active")) }, + 2: () => { generateListGroupUSSD($("#accordion-ussd .ui-accordion-content-active")) }, + 3: () => {} + }; $("button").button(); - $("#tabs").tabs(); - // $("#accordion-numbers").accordion(); + $("#update-tab").button("option", "icon", "ui-icon-refresh"); + $("#update-group").button("option", "icon", "ui-icon-arrowrefresh-1-s"); + $("#add-number").button("option", "icon", "ui-icon-plusthick"); + $("#logout").button("option", "icon", "ui-icon-power"); - // $(".addNumber").click(() => { - // numberAdd() - // }); - - $("body").fadeTo(500, 1); - - // getData("Список номеров успешно загружен"); - - $(".search").on("input", function () { - // showNumbers(numbers.filter(e => e.id.includes($(this).val()))) - }).keydown(function (e) { - // e.key == "Escape" && ($(this).val(""), showNumbers()) + $("#tabs").tabs({ + activate: function( event, ui ) { + lists[$(this).tabs( "option", "active" )](); + $("#add-number").button( "option", "disabled", $(this).tabs( "option", "active" ) > 0 ); + $("#update-group").button( "option", "disabled", $(this).tabs( "option", "active" ) > 2 ); + } }); - loadData(); + $("#update-tab").click(() => { + tabs[$("#tabs").tabs( "option", "active" )]() + }); + + $("#update-group").click(() => { + groups[$("#tabs").tabs( "option", "active" )]() + }); + + $("#add-number").click(() => { + addNumber($("#accordion-numbers .ui-accordion-content-active")) + }); + + $("#search").on("input", function () { + lists[$("#tabs").tabs( "option", "active" )]() + }).keydown(function (e) { + e.key == "Escape" && ($(this).val(""), lists[$("#tabs").tabs( "option", "active" )]()) + }); + + $("#logout").click(() => { + request('logout', 'json').then(data => { + data.error ? noticer.error(data.message) : (window.location.href = "."); + }).catch(error => { + noticer.error(error.message); + }); + }); + + loadNumbers(); + loadSMS(); + loadUSSD(); + loadServerInfo(); + + $("body").fadeTo(500, 1); }) async function request(query, type, queryData = {}) { @@ -51,40 +101,591 @@ function isJSON(str) { } } -function loadData() { - request('listsgroups', 'json').then(data => { - data.error ? message.error(data.message) : generateListsGroups(data); +function isNumeric(value) { + return /^-?\d+$/.test(value); +} + +function divNotFoundNumbers(text = '') { + let notFound = $('.notFoundNumbers'); + let divTable = $('.body-rows'); + let divNotFound = $(`
${text}
`); + + divNotFound.css({ + "color": "#333", + "text-align": "center", + "padding": "20px 0 20px 0" + }); + + this.push = function() { + divTable.append(divNotFound); + } + + this.remove = function() { + notFound.remove(); + } +} + +function pEmpty(text) { + return $(`

${text}

`).css('text-align', 'center'); +} + +/************************************************************************************ + + Обработка таблицы с номерами телефонов + +************************************************************************************/ + +function loadNumbers() { + request('listnumbergroups', 'text').then(data => { + if (isJSON(data) && JSON.parse(data).error) + noticer.error(JSON.parse(data).message); + else + generateListNumberGroups(data); }).catch(error => { - message.error(error.message); + noticer.error(error.message); }); } -function generateListsGroups(data) { - let numbers = $("#tabs-numbers"); - let group = $('
'); - $(data).each((i, j) => { - group.append(`

${j.comment}

`); - }); - numbers.append(group); +function generateListNumberGroups(data) { + if (!$(data).children().length) { + $("#tabs-numbers").html(pEmpty('Номера телефонов отсутствуют')); + return; + } + + $("#tabs-numbers").html(data); $("#accordion-numbers").accordion({ heightStyle: "content", create: function( event, ui ) { - generateGroupNumbers(ui.panel); + generateListGroupNumbers(ui.panel); }, beforeActivate: function( event, ui ) { - generateGroupNumbers(ui.newPanel); + generateListGroupNumbers(ui.newPanel); } }); } -async function generateGroupNumbers(panel) { - request('groupnumbers', 'text', { group: panel.data("group-name") }).then(data => { - if (isJSON(data) && JSON.parse(data).error) - message.error(JSON.parse(data).message); +function generateListGroupNumbers(panel) { + if (!$("#accordion-numbers").children().length) { + noticer.warning("Номера телефонов отсутствуют"); + $("#tabs-numbers").html(pEmpty('Номера телефонов отсутствуют')); + return; + } + + request('listgroupnumbers', 'json', { group: panel.data("group-name") }).then(data => { + if (data.error) + noticer.error(data.message); else { - panel.html(data); + numbers = data; + showListNumbers(panel); } }).catch(error => { - message.error(error.message); + noticer.error(error.message); + }); +} + +function showListNumbers(panel, data = numbers.filter(e => e.number.includes($("#search").val()))) { + (new divNotFoundNumbers).remove(); + let body = panel.find('.body').html(''); + $(data).each((i, j) => { + let row = $(``); + row.append(`${j.number}`); + row.append(`${j.list}`); + row.append(`${j.all_cc}`); + row.append(`${j.white_cc}`); + row.append(`${j.black_cc}`); + row.append(`${j.comment}`); + body.append(row); + + row.click(function() { + viewNumber(panel, $(this).data('number')); + }); + }); + + if (!body.children().length) + (new divNotFoundNumbers('Нет номеров')).push(); +} + +function viewNumber(panel, number) { + request('viewnumber', 'text', {number: number}).then(data => { + if (isJSON(data) && JSON.parse(data).error) + noticer.error(JSON.parse(data).message); + else { + showViewNumber(data, [ + { + id: "btn-save", + text: "Сохранить", + icon: "ui-icon-check", + click: function() { + actionNumber(panel, $(this), 'updatenumber'); + } + }, + { + id: "btn-delete", + text: "Удалить", + icon: "ui-icon-trash", + click: function() { + delNumber(panel, $(this)); + } + } + ], `Редактирование номера ${number}`); + } + }).catch(error => { + noticer.error(error.message); + }); +} + +function showViewNumber(data, actionButton, title) { + let form = $(data); + + form.appendTo('body').dialog({ + title: title, + height: 'auto', + width: 'auto', + resizable: false, + modal: true, + show: { effect: "fade", duration: 500 }, + close: function(event, ui) { + $(this).dialog('destroy').remove() + }, + buttons: [ + ...actionButton, + { + text: "Отмена", + icon: "ui-icon-cancel", + click: function() { + $(this).dialog("close"); + } + } + ] + }); + + $('#number-group, #number-list').selectmenu({ + width: 200 + }); +} + +function actionNumber(panel, currentWindow, query) { + let pattern_number = /^\+7\d{10}$/g; + + let number = $('#number-number').val(); + let group = $('#number-group').val(); + let list = $('#number-list').val(); + let all_cc = $('#number-all-cc').val(); + let white_cc = $('#number-white-cc').val(); + let black_cc = $('#number-black-cc').val(); + let comment = $('#number-comment').val(); + + let error = false; + + if (number.match(pattern_number) === null) { noticer.warning("Номер не соответствует формату +7XXXXXXXXXX"); error = true; } + if (!isNumeric(all_cc)) { noticer.warning("Общее количество звонков должно быть числом"); error = true; } + if (all_cc < 0) { noticer.warning("Общее количество звонков не может быть отрицательным"); error = true; } + if (!isNumeric(white_cc)) { noticer.warning("Белое количество звонков должно быть числом"); error = true; } + if (white_cc < 0) { noticer.warning("Белое количество звонков не может быть отрицательным"); error = true; } + if (!isNumeric(black_cc)) { noticer.warning("Черное количество звонков должно быть числом"); error = true; } + if (black_cc < 0) { noticer.warning("Черное количество звонков не может быть отрицательным"); error = true; } + + if (error) return; + + request(query, 'json', { + number: number, + group: group, + list: list, + all_cc: parseInt(all_cc), + white_cc: parseInt(white_cc), + black_cc: parseInt(black_cc), + comment: comment + }).then(data => { + if (data.error) + noticer.error(data.message); + else + { + query == 'write' ? noticer.success(`Номер ${number} был добавлен`) : noticer.success(`Номер ${number} был обновлен`); + generateListGroupNumbers(panel); + currentWindow.dialog("close"); + } + }).catch(error => { + noticer.error(error.message); + }); +} + +function addNumber(panel) { + request('addnumber', 'text', { group: panel.data("group-name") }).then(data => { + if (isJSON(data) && JSON.parse(data).error) + noticer.error(JSON.parse(data).message); + else { + showViewNumber(data, [{ + id: "btnSave", + text: "Добавить", + icon: "ui-icon-check", + click: function() { + actionNumber(panel, $(this), 'writenumber'); + } + }], "Добавление нового номера"); + } + }).catch(error => { + noticer.error(error.message); + }); +} + +function delNumber(panel, currentWindow) { + let number = $('#number-number').val(); + + let error = false; + if (!number.length) { noticer.warning('Номер не может быть пуст'); error = true; } + if (error) return; + + request('delnumber', 'json', { + number: number + }).then(data => { + if (data.error) + noticer.error(data.message); + else { + noticer.success(`Номер ${number} был удален`); + generateListGroupNumbers(panel); + currentWindow.dialog("close"); + } + }).catch(error => { + noticer.error(error.message); + }); +} + +/************************************************************************************ + + Обработка таблицы с SMS + +************************************************************************************/ + +function loadSMS() { + request('listsmsgroups', 'text').then(data => { + if (isJSON(data) && JSON.parse(data).error) + noticer.error(JSON.parse(data).message); + else + generateListSMSGroups(data); + }).catch(error => { + noticer.error(error.message); + }); +} + +function generateListSMSGroups(data) { + if (!$(data).children().length) { + $("#tabs-sms").html(pEmpty('SMS отсутствуют')); + return; + } + + $("#tabs-sms").html(data); + $("#accordion-sms").accordion({ + heightStyle: "content", + create: function( event, ui ) { + generateListGroupSMS(ui.panel); + }, + beforeActivate: function( event, ui ) { + generateListGroupSMS(ui.newPanel); + } + }) +} + +function generateListGroupSMS(panel) { + if (!$("#accordion-sms").children().length) { + noticer.warning("SMS отсутствуют"); + $("#tabs-sms").html(pEmpty('SMS отсутствуют')); + return; + } + + request('listgroupsms', 'json', { to: panel.data("to") }).then(data => { + if (data.error) + noticer.error(data.message); + else { + sms = data; + showListSMS(panel); + } + }).catch(error => { + noticer.error(error.message); + }); +} + +function showListSMS(panel, data = sms.filter(e => e.from.includes($("#search").val()))) { + (new divNotFoundNumbers).remove(); + let body = panel.find('.body').html(''); + $(data).each((i, j) => { + let row = $(``); + row.append(`${j.date}`); + row.append(`${j.from}`); + row.append(`${j.text}`); + body.append(row); + + row.click(function() { + viewSMS(panel, $(this).data('sms-id'), j.from); + }); + }); + + if (!body.children().length) + (new divNotFoundNumbers('Нет SMS')).push(); +} + +function viewSMS(panel, id, number) { + request('viewsms', 'text', {id: id}).then(data => { + if (isJSON(data) && JSON.parse(data).error) + noticer.error(JSON.parse(data).message); + else { + showViewSMS(data, [ + { + id: "btn-delete", + text: "Удалить", + icon: "ui-icon-trash", + click: function() { + delSMS(panel, $(this)); + } + } + ], `SMS от ${number}`); + } + }).catch(error => { + noticer.error(error.message); + }); +} + +function showViewSMS(data, actionButton, title) { + let form = $(data); + + form.appendTo('body').dialog({ + title: title, + height: 'auto', + width: 'auto', + resizable: false, + modal: true, + show: { effect: "fade", duration: 500 }, + close: function(event, ui) { + $(this).dialog('destroy').remove() + }, + buttons: [ + ...actionButton, + { + text: "Отмена", + icon: "ui-icon-cancel", + click: function() { + $(this).dialog("close"); + } + } + ] + }); +} + +function delSMS(panel, currentWindow) { + let id = $('#sms-content').data('id'); + let from = $('#sms-content').data('from'); + + request('delsms', 'json', { + id: id + }).then(data => { + if (data.error) + noticer.error(data.message); + else { + noticer.success(`SMS от ${from} было удалено`); + generateListGroupSMS(panel); + currentWindow.dialog("close"); + } + }).catch(error => { + noticer.error(error.message); + }); +} + +/************************************************************************************ + + Обработка таблицы с USSD + +************************************************************************************/ + +function loadUSSD() { + request('listussdgroups', 'text').then(data => { + if (isJSON(data) && JSON.parse(data).error) + noticer.error(JSON.parse(data).message); + else + generateListUSSDGroups(data); + }).catch(error => { + noticer.error(error.message); + }); +} + +function generateListUSSDGroups(data) { + if (!$(data).children().length) { + $("#tabs-ussd").html(pEmpty('USSD отсутствуют')); + return; + } + + $("#tabs-ussd").html(data); + $("#accordion-ussd").accordion({ + heightStyle: "content", + create: function( event, ui ) { + generateListGroupUSSD(ui.panel); + }, + beforeActivate: function( event, ui ) { + generateListGroupUSSD(ui.newPanel); + } + }) +} + +function generateListGroupUSSD(panel) { + if (!$("#accordion-ussd").children().length) { + noticer.warning("USSD отсутствуют"); + $("#tabs-ussd").html(pEmpty('USSD отсутствуют')); + return; + } + + request('listgroupussd', 'json', { to: panel.data("to") }).then(data => { + if (data.error) + noticer.error(data.message); + else { + ussd = data; + showListUSSD(panel); + } + }).catch(error => { + noticer.error(error.message); + }); +} + +function showListUSSD(panel, data = ussd) { + (new divNotFoundNumbers).remove(); + let body = panel.find('.body').html(''); + $(data).each((i, j) => { + let row = $(``); + row.append(`${j.date}`); + row.append(`${j.type_comment}`); + row.append(`${j.text}`); + body.append(row); + + row.click(function() { + viewUSSD(panel, $(this).data('ussd-id'), j.type_comment); + }); + }); + + if (!body.children().length) + (new divNotFoundNumbers('Нет USSD')).push(); +} + +function viewUSSD(panel, id, type) { + request('viewussd', 'text', {id: id}).then(data => { + if (isJSON(data) && JSON.parse(data).error) + noticer.error(JSON.parse(data).message); + else { + showViewUSSD(data, [ + { + id: "btn-delete", + text: "Удалить", + icon: "ui-icon-trash", + click: function() { + delUSSD(panel, $(this)); + } + } + ], `USSD: ${type}`); + } + }).catch(error => { + noticer.error(error.message); + }); +} + +function showViewUSSD(data, actionButton, title) { + let form = $(data); + + form.appendTo('body').dialog({ + title: title, + height: 'auto', + width: 'auto', + resizable: false, + modal: true, + show: { effect: "fade", duration: 500 }, + close: function(event, ui) { + $(this).dialog('destroy').remove() + }, + buttons: [ + ...actionButton, + { + text: "Отмена", + icon: "ui-icon-cancel", + click: function() { + $(this).dialog("close"); + } + } + ] + }); +} + +function delUSSD(panel, currentWindow) { + let id = $('#ussd-content').data('id'); + + request('delussd', 'json', { + id: id + }).then(data => { + if (data.error) + noticer.error(data.message); + else { + noticer.success(`USSD было удалено`); + generateListGroupUSSD(panel); + currentWindow.dialog("close"); + } + }).catch(error => { + noticer.error(error.message); + }); +} + +/************************************************************************************ + + Обработка таблицы информации о сервере + +************************************************************************************/ + +function loadServerInfo() { + request('serverinfo', 'text').then(data => { + if (isJSON(data) && JSON.parse(data).error) + noticer.error(JSON.parse(data).message); + else + showServerInfo(data); + }).catch(error => { + noticer.error(error.message); + }); +} + +function showServerInfo(data) { + $("#tabs-server").html(data); + $("#server-external-number-on").checkboxradio(); + $("#server-transparent-mode").checkboxradio(); + $("#server-button").button({ icon: "ui-icon-disk", disabled: true }); + + $(".server-input").on("change paste cut keydown", () => { + if ($("#server-button").button( "option", "disabled")) { + noticer.warning('Некоторые параметры сервера были изменены'); + $("#server-button").button( "option", "disabled", false); + } + }); + + $("#server-button").click(() => { + writeServerInfo(); + }); +} + +function writeServerInfo() { + let pattern_number = /^\+7\d{10}$/g; + + let internal_number = $("#server-internal-number").val(); + let external_number = $("#server-external-number").val(); + let external_number_on = $("#server-external-number-on").is(":checked"); + let transparent_mode = $("#server-transparent-mode").is(":checked"); + + let error = false; + + if (external_number.match(pattern_number) === null) { noticer.warning("Внешний номер не соответствует формату +7XXXXXXXXXX"); error = true; } + + if (error) return; + + request('writeserverinfo', 'json', { + internal_number: internal_number, + external_number: external_number, + external_number_on: external_number_on, + transparent_mode: transparent_mode + }).then(data => { + data.error ? + noticer.error(data.message) : + $("#server-button").button( "option", "disabled", true) && noticer.success("Параметры сервера были сохранены") + }).catch(error => { + noticer.error(error.message); }); } diff --git a/public/404.css b/public/404.css new file mode 100644 index 0000000..74f4081 --- /dev/null +++ b/public/404.css @@ -0,0 +1,18 @@ +body { + display: flex; + align-items: center; + justify-content: center; + height: 100vh; +} + +div.page { + display: flex; + align-items: center; +} + +.pic { + background-image: url("404.png"); + min-width: 512px; + min-height: 512px; + background-size: contain; +} diff --git a/public/authorization.css b/public/authorization.css new file mode 100644 index 0000000..8137a7a --- /dev/null +++ b/public/authorization.css @@ -0,0 +1,62 @@ +@font-face { + font-family: Scada; + src: url(Scada-Regular.ttf); +} + +body { + display: flex; + align-items: center; + justify-content: center; + height: 100vh; + margin-top: -5%; + color: #333; + opacity: 0; + font-family: Scada; +} + +div.form { + display: flex; + flex-direction: column; + align-items: center; +} + +.div-button { + display: flex; + justify-content: center; + margin-top: 10px; +} + +input { + text-align: center; + color: #333; + border: 1px solid#c5c5c5; + height: 30px; +} + +input:hover { + border: 1px solid #999; + box-shadow: 1px 1px 10px 1px #ccc; +} + +.input-focus:focus { + outline: none; + box-shadow: 1px 1px 10px 1px #666; + border: 1px solid #555; +} + +.logo { + background-image: url("favicon.png"); + min-width: 128px; + min-height: 128px; + background-size: contain; + margin-bottom: 20px; +} + +.title { + margin: 30px; +} + +.label { + text-align: right; + padding-right: 10px; +} diff --git a/public/style.css b/public/style.css index 3f86680..8cd066b 100644 --- a/public/style.css +++ b/public/style.css @@ -18,16 +18,16 @@ body { div.div-header { display: flex; align-items: center; - width: 60%; + width: 70%; justify-content: center; margin-top: 30px } /* HEADER */ -/* div.div-add { +div.main-button { margin-right: 20px -} */ +} div.div-search { display: flex; @@ -49,36 +49,41 @@ input { border: 1px solid#c5c5c5; } -.input-focus:hover { - border: 1px solid #ccc +.input-focus:hover:not([disabled]) { + border: 1px solid #999; + box-shadow: 1px 1px 10px 1px #ccc; } .input-focus::placeholder { color: #333 } -.input-focus:focus { +.input-focus:focus:not([disabled]) { outline: none; - box-shadow: 1px 1px 10px 1px #007fff; - border: 1px solid #003eff; + box-shadow: 1px 1px 10px 1px #666; + border: 1px solid #555; } /* BODY */ .content { - width: 60%; + width: 70%; margin-top: 20px } -.content table { +.table-content { width: 100%; border-collapse: collapse; border-spacing: 0; table-layout: fixed; } +div.group-content { + height: 55vh; +} + div.body-rows { - max-height: 55vh; + height: calc(100% - 55px); overflow-x: auto; border: 1px solid #c5c5c5; border-top: 0; @@ -96,12 +101,112 @@ td { text-align: center; } +.body-rows td { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .body-rows tbody tr:nth-child(even){ background: #fff; } -tr.row:hover, tr.row:nth-child(even):hover { - background-color: #c5c5c5; - color: #fff; - cursor: pointer; +tr.row { + border-top: 1px solid #fff; +} + +tr.row:hover, tr.row:nth-child(even):hover { + background-color: #f6f6f6; + color: #003eff; + cursor: pointer; + border-bottom: 1px solid #c5c5c5; + border-top: 1px solid #c5c5c5; +} + +/* EDIT NUMBER */ + +.number-label, .sms-label, .ussd-label, .server-label { + color: #333; + text-align: right; +} + +.number-value, .server-value { + height: 30px; +} + +.server-value { + text-align: left; + padding: 2px 0 2px 10px; +} + +.server-input { + height: 100%; +} + +.server-button { + margin-top: 30px; +} + +.sms-label-text, .ussd-label-text { + vertical-align:top +} + +.sms-value, .ussd-value { + width: 300px; + text-align: left; +} + +.number-input-main { + height: 25px; + width: 194px; + margin: 0 10px 0 10px; +} + +.number-input-cc { + height: 25px; + width: 50px; + margin: 0 10px 0 10px; +} + +.div-advanced { + display: flex; + flex-direction: column; +} + +.comment { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.comment-name { + padding: 15px 0 5px 0; +} + +.comment-content, .sms-content, .ussd-content { + width: 100%; + height: 100%; +} + +#number-comment, #sms-content, #ussd-content { + width: calc(100% - 6px); + resize: none; +} + +#sms-content, #ussd-content { + height: 150px; + outline: none; + border: 1px solid#c5c5c5; + color: #333; + width: 400px; + padding: 5px 10px 5px 10px; +} + +th.sms-content-width, td.sms-content-width, th.ussd-content-width, td.ussd-content-width { + width: 155px; +} + +th.ussd-content-width-type, td.ussd-content-width-type { + width: 240px; } diff --git a/settings.conf.sample b/settings.conf.sample index 8a98fe3..538d199 100644 --- a/settings.conf.sample +++ b/settings.conf.sample @@ -1,14 +1,18 @@ [web-host] title => "Управление диалпланом" addresses => 127.0.0.1 -http => 8080 +http => 80 https => 443 cert => certs/test.local.crt key => certs/test.local.key -data => ./ ; Путь к каталогу, в котором расположены каталоги public, js, images +data => ./ ; Путь к каталогу, в котором расположены каталоги public, jq, js, images loglevel => 0 ; 0 - debug, 1 - crit, 2 - err, 3 - warn, 4 - notice, 5 - info, 6 - alert logoutput => 1, 4 ; 1 - syslog, 2 - stout, 4 - file => example: 1,2 or 1,2,4 -logfile => /var/log/jaster.log ; if log-output set with 4 +logfile => /var/log/daster.log ; if log-output set with 4 + +[auth] +login => +password => [daster-db] host => 127.0.0.1 diff --git a/source/daster.d b/source/daster.d index 7c2c52a..0f4876c 100644 --- a/source/daster.d +++ b/source/daster.d @@ -15,19 +15,29 @@ import std.array; import verinfo; import pgdb; import structures; +import response; -import requests.listsgroups; -import requests.groupnumbers; +import requests.numbers; +import requests.sms; +import requests.ussd; +import requests.server; +import requests.authorization; static ServerInfo serverInfo; +static AuthData serverAuthData; private void showVersion() { writefln("daster версия %s, собрано %s", getDasterVersion(), __DATE__); } +void page404(HTTPServerRequest req, HTTPServerResponse res, HTTPServerErrorInfo error) { + res.render!("404.dt", req, error); +} + int main(string[] args) { - log.level(log.INFORMATION); - log.output(log.SYSLOG); + log.level(log.INFORMATION) + .output(log.SYSLOG) + .color(true); bool flagVersion; string flagSettings; @@ -73,15 +83,17 @@ int main(string[] args) { } rc.read(flagSettings); - rcAsteriskDB(); - auto webHost = rcWebHost(); + auto webHost = rcWebHost(); serverInfo = ServerInfo(webHost.title); if (webHost.loglevel != -1) log.level(webHost.loglevel); if (webHost.logoutput) log.output(webHost.logoutput); if (webHost.logfile.length) log.file(webHost.logfile); + rcAsteriskDB(); + rcAuth(); + auto router = new URLRouter; router.post("/", &postReq); router.get("/", &getReq); @@ -96,12 +108,14 @@ int main(string[] args) { auto settingsHTTPS = new HTTPServerSettings; if (webHost.http) { + settingsHTTP.errorPageHandler = toDelegate(&page404); settingsHTTP.sessionStore = memorySessionStore; settingsHTTP.port = webHost.http; settingsHTTP.bindAddresses = ["::1"] ~ webHost.addresses; } if (webHost.https) { + settingsHTTPS.errorPageHandler = toDelegate(&page404); settingsHTTPS.sessionStore = memorySessionStore; settingsHTTPS.port = webHost.https; settingsHTTPS.bindAddresses = ["::1"] ~ webHost.addresses; @@ -135,16 +149,15 @@ void startWebServer(WebHost wh, HTTPServerSettings http, HTTPServerSettings http } void getReq(HTTPServerRequest req, HTTPServerResponse res) { - // if (req.session) { - // auto user = req.session.get!UserData("userData"); - // if (user.loggedIn) { - // renderMainPage(req, res); - // return; - // } - // } + if (req.session) { + auto user = req.session.get!UserData("user"); + if (user.login) { + renderMainPage(req, res); + return; + } + } - // render!("index.dt", serverInfo)(res); - renderMainPage(req, res); + render!("authorization.dt", serverInfo)(res); } void renderMainPage(HTTPServerRequest req, HTTPServerResponse res) { @@ -159,45 +172,74 @@ void postReq(HTTPServerRequest req, HTTPServerResponse res) { if (query.empty) return; - // if (query != "authorization" && !checkAuth(req)) { - // res.send( - // true, - // "Сессия не существует. Перезагрузите страницу" - // ); - // return; - // } + if (query != "login" && !checkAuth(req)) { + res.send( + true, + "Сессия не существует. Перезагрузите страницу" + ); + return; + } + + log.d("%s: json request %s".format(req.clientAddress.toAddressString(), jsr.to!string)); switch (query) { - case "listsgroups": - listsgroups(req, res); + case "login": + login(req, res, serverAuthData); break; - case "groupnumbers": - groupnumbers(req, res); + case "logout": + logout(req, res); + break; + case "listnumbergroups": + getListNumberGroups(req, res); + break; + case "listgroupnumbers": + getListGroupNumbers(req, res); + break; + case "viewnumber": + getViewNumber(req, res); + break; + case "addnumber": + getAddNumber(req, res); + break; + case "writenumber": + sendWriteNumber(req, res); + break; + case "updatenumber": + sendUpdateNumber(req, res); + break; + case "delnumber": + sendDelNumber(req, res); + break; + case "listsmsgroups": + getListSMSGroups(req, res); + break; + case "listgroupsms": + getListGroupSMS(req, res); + break; + case "viewsms": + getViewSMS(req, res); + break; + case "delsms": + sendDelSMS(req, res); + break; + case "listussdgroups": + getListUSSDGroups(req, res); + break; + case "listgroupussd": + getListGroupUSSD(req, res); + break; + case "viewussd": + getViewUSSD(req, res); + break; + case "delussd": + sendDelUSSD(req, res); + break; + case "serverinfo": + getServerInfo(req, res); + break; + case "writeserverinfo": + sendWriteServerInfo(req, res); break; - // case "authorization": - // authorization(req, res); - // break; - // case "logout": - // logout(req, res); - // break; - // case "numbers": - // numbers(req, res); - // break; - // case "add": - // addNumber(req, res); - // break; - // case "write": - // writeNumber(req, res); - // break; - // case "edit": - // editNumber(req, res); - // break; - // case "update": - // updateNumber(req, res); - // break; - // case "remove": - // removeNumber(req, res); - // break; default: res.redirect("/"); } @@ -242,7 +284,7 @@ void rcAsteriskDB() { " password=" ~ asteriskDB["password"] ); } catch (Exception e) { - log.c(e); + log.c(e.msg); exit(1); } } @@ -403,3 +445,24 @@ WebHost rcWebHost() { return wh; } + +void rcAuth() { + ConfigSection auth; + + try { + auth = rc[]["auth"]; + } catch (Exception e) { + log.c("В конфигурационном файле не верны настройки auth"); + exit(1); + } + + if (auth["login"].empty) + log.w("Логин не был установлен - auth.login"); + else + serverAuthData.login = auth["login"]; + + if (auth["password"].empty) + log.w("Пароль не был установлен - auth.password"); + else + serverAuthData.password = auth["password"]; +} diff --git a/source/data.d b/source/data.d deleted file mode 100644 index b333388..0000000 --- a/source/data.d +++ /dev/null @@ -1,67 +0,0 @@ -module data; - -import pgdb; -import singlog; -import structures; - -import std.conv; - -GroupDB[] getListGroups() { - GroupDB[] groups; - try { - auto queryResult = pgsql.sql( - "select distinct - n.da_group, - g.da_comment - from da_numbers n - left join da_groups g ON g.da_name = n.da_group" - ); - foreach (row; queryResult) { - GroupDB data; - - data.name = row["da_group"]; - data.comment = row["da_comment"]; - - groups ~= data; - } - } catch (Exception e) { - log.e("Не удалось выполнить запрос к БД. " ~ e.msg); - } - - return groups; -} - -NumberDB[] getListNumbers(string group) { - NumberDB[] numbers; - try { - auto queryResult = pgsql.sql( - "select - dan.da_number, - dal.da_comment da_list, - dan.da_all_cc, - dan.da_white_cc, - dan.da_black_cc, - dan.da_comment - from da_numbers dan - left join da_lists dal on dal.da_name = dan.da_list - where da_group = ?", - group - ); - foreach (row; queryResult) { - NumberDB data; - - data.number = row["da_number"]; - data.list = row["da_list"]; - data.all_cc = row["da_all_cc"].to!int; - data.white_cc = row["da_white_cc"].to!int; - data.black_cc = row["da_black_cc"].to!int; - data.comment = row["da_comment"]; - - numbers ~= data; - } - } catch (Exception e) { - log.e("Не удалось выполнить запрос к БД. " ~ e.msg); - } - - return numbers; -} diff --git a/source/requests/authorization.d b/source/requests/authorization.d new file mode 100644 index 0000000..5456424 --- /dev/null +++ b/source/requests/authorization.d @@ -0,0 +1,47 @@ +module requests.authorization; + +import vibe.vibe; +import response; +import structures; +import singlog; + +void login(HTTPServerRequest req, HTTPServerResponse res, AuthData serverAuthData) { + auto userAuthData = deserializeJson!AuthData(req.json); + + if (!(serverAuthData.login == userAuthData.login && + serverAuthData.password == userAuthData.password)) { + log.i(req.clientAddress.toAddressString() ~ ": Данные авторизации не верны"); + res.send( + true, + "Данные авторизации не верны" + ); + return; + } + + auto user = UserData(true); + + req.session = res.startSession(); + req.session.set!UserData("user", user); + + log.i(req.clientAddress.toAddressString() ~ ": Вход в систему"); + + res.send(); +} + +void logout(HTTPServerRequest req, HTTPServerResponse res) { + req.session.set!UserData("user", UserData.init); + res.terminateSession(); + + log.i(req.clientAddress.toAddressString() ~ ": Выход из системы"); + + res.send(); +} + +bool checkAuth(HTTPServerRequest req) { + if (req.session) + return req.session.get!UserData("user").login; + + log.d(req.clientAddress.toAddressString() ~ ": Отсутствует авторизация"); + + return false; +} diff --git a/source/requests/groupnumbers.d b/source/requests/groupnumbers.d deleted file mode 100644 index 4634a38..0000000 --- a/source/requests/groupnumbers.d +++ /dev/null @@ -1,12 +0,0 @@ -module requests.groupnumbers; - -import vibe.vibe; -import response; -import data; -import singlog; - -void groupnumbers(HTTPServerRequest req, HTTPServerResponse res) { - auto jsr = req.json; - auto listNumbers = getListNumbers(jsr["group"].get!string); - render!("group-numbers-list.dt", listNumbers)(res); -} diff --git a/source/requests/listsgroups.d b/source/requests/listsgroups.d deleted file mode 100644 index be1504c..0000000 --- a/source/requests/listsgroups.d +++ /dev/null @@ -1,11 +0,0 @@ -module requests.listsgroups; - -import vibe.vibe; -import response; -import data; - -void listsgroups(HTTPServerRequest req, HTTPServerResponse res) { - // auto jsr = req.json; - - res.writeJsonBody(getListGroups().serializeToJson()); -} diff --git a/source/requests/numbers.d b/source/requests/numbers.d new file mode 100644 index 0000000..24fa4d7 --- /dev/null +++ b/source/requests/numbers.d @@ -0,0 +1,113 @@ +module requests.numbers; + +import vibe.vibe; +import response; +import structures; +import sql; +import singlog; + +import std.regex; + +// Получить список всех групп номеров +void getListNumberGroups(HTTPServerRequest req, HTTPServerResponse res) { + auto listGroups = sqlGetListGroups(); + render!("list_number_groups.dt", listGroups)(res); +} + +// Получить список номеров конкретной группы +void getListGroupNumbers(HTTPServerRequest req, HTTPServerResponse res) { + auto jsr = req.json; + res.writeJsonBody(sqlGetListNumbers(jsr["group"].get!string).serializeToJson()); +} + +// Добавление номера телефона +void getAddNumber(HTTPServerRequest req, HTTPServerResponse res) { + auto jsr = req.json; + bool edit = false; + NumberDB dataNumber; + dataNumber.group = jsr["group"].get!string; + auto groups = sqlGetGroups(); + auto lists = sqlGetLists(); + render!("number.dt", edit, dataNumber, groups, lists)(res); +} + +// Просмотр номера телефона +void getViewNumber(HTTPServerRequest req, HTTPServerResponse res) { + auto jsr = req.json; + bool edit = true; + auto dataNumber = sqlGetDataNumber(jsr["number"].get!string); + auto groups = sqlGetGroups(); + auto lists = sqlGetLists(); + render!("number.dt", edit, dataNumber, groups, lists)(res); +} + +// Обновить номер телефона +void sendUpdateNumber(HTTPServerRequest req, HTTPServerResponse res) { + NumberDB number = deserializeJson!NumberDB(req.json); + + if (!checkNumber(number, res)) + return; + + if (!sqlUpdateNumber(number)) { + res.send(true, "Не удалось обновить номер"); + return; + } + res.send(); +} + +// Записать номер телефона +void sendWriteNumber(HTTPServerRequest req, HTTPServerResponse res) { + NumberDB number = deserializeJson!NumberDB(req.json); + + if (!checkNumber(number, res)) + return; + + if (!sqlInsertNumber(number)) { + res.send(true, "Не удалось записать номер"); + return; + } + res.send(); +} + +// Удалить номера телефона +void sendDelNumber(HTTPServerRequest req, HTTPServerResponse res) { + auto jsr = req.json; + string number = jsr["number"].get!string; + + if (!number.length) { + res.send(true, "Номер не может быть пуст"); + return; + } + + if (!sqlDeleteNumber(number)) { + res.send(true, "Не удалось удалить номер"); + return; + } + + res.send(); +} + +// Проверка номера перед изменением +bool checkNumber(NumberDB number, HTTPServerResponse res) { + if (!number.number.matchFirst(regex(r"^\+7\d{10}$", "g"))) { + res.send(true, "Номер не соответствует формату +7XXXXXXXXXX"); + return false; + } + + if (number.all_cc < 0) { + res.send(true, "Общее количество звонков не может быть отрицательным"); + return false; + } + + if (number.white_cc < 0) { + res.send(true, "Белое количество звонков не может быть отрицательным"); + return false; + } + + if (number.black_cc < 0) { + res.send(true, "Черное количество звонков не может быть отрицательным"); + return false; + } + + return true; +} diff --git a/source/requests/server.d b/source/requests/server.d new file mode 100644 index 0000000..7b66583 --- /dev/null +++ b/source/requests/server.d @@ -0,0 +1,30 @@ +module requests.server; + +import vibe.vibe; +import response; +import structures; +import sql; +import singlog; + +import std.regex; + +// Получить информацию о сервере +void getServerInfo(HTTPServerRequest req, HTTPServerResponse res) { + auto dataServer = sqlGetServerInfo(); + render!("server.dt", dataServer)(res); +} + +void sendWriteServerInfo(HTTPServerRequest req, HTTPServerResponse res) { + ServerDB server = deserializeJson!ServerDB(req.json); + + if (!server.external_number.matchFirst(regex(r"^\+7\d{10}$", "g"))) { + res.send(true, "Внешний номер не соответствует формату +7XXXXXXXXXX"); + return; + } + + if (!sqlUpdateServerInfo(server)) { + res.send(true, "Не удалось записать параметры сервера"); + return; + } + res.send(); +} diff --git a/source/requests/sms.d b/source/requests/sms.d new file mode 100644 index 0000000..254793b --- /dev/null +++ b/source/requests/sms.d @@ -0,0 +1,39 @@ +module requests.sms; + +import vibe.vibe; +import response; +import structures; +import sql; +import singlog; + +// Получить список всех групп SMS +void getListSMSGroups(HTTPServerRequest req, HTTPServerResponse res) { + auto numbers = sqlGetSMSNumbers(); + render!("list_sms_groups.dt", numbers)(res); +} + +// Получить список SMS конкретной группы +void getListGroupSMS(HTTPServerRequest req, HTTPServerResponse res) { + auto jsr = req.json; + res.writeJsonBody(sqlGetListSMS(jsr["to"].get!string).serializeToJson()); +} + +// Просмотр SMS +void getViewSMS(HTTPServerRequest req, HTTPServerResponse res) { + auto jsr = req.json; + auto dataSMS = sqlGetSMS(jsr["id"].to!int); + render!("sms.dt", dataSMS)(res); +} + +// Удалить SMS +void sendDelSMS(HTTPServerRequest req, HTTPServerResponse res) { + auto jsr = req.json; + int idsms = jsr["id"].get!int; + + if (!sqlDeleteSMS(idsms)) { + res.send(true, "Не удалось удалить SMS"); + return; + } + + res.send(); +} diff --git a/source/requests/ussd.d b/source/requests/ussd.d new file mode 100644 index 0000000..c39a46e --- /dev/null +++ b/source/requests/ussd.d @@ -0,0 +1,39 @@ +module requests.ussd; + +import vibe.vibe; +import response; +import structures; +import sql; +import singlog; + +// Получить список всех групп USSD +void getListUSSDGroups(HTTPServerRequest req, HTTPServerResponse res) { + auto numbers = sqlGetUSSDNumbers(); + render!("list_ussd_groups.dt", numbers)(res); +} + +// Получить список USSD конкретной группы +void getListGroupUSSD(HTTPServerRequest req, HTTPServerResponse res) { + auto jsr = req.json; + res.writeJsonBody(sqlGetListUSSD(jsr["to"].get!string).serializeToJson()); +} + +// Просмотр USSD +void getViewUSSD(HTTPServerRequest req, HTTPServerResponse res) { + auto jsr = req.json; + auto dataUSSD = sqlGetUSSD(jsr["id"].to!int); + render!("ussd.dt", dataUSSD)(res); +} + +// Удалить USSD +void sendDelUSSD(HTTPServerRequest req, HTTPServerResponse res) { + auto jsr = req.json; + int idussd = jsr["id"].get!int; + + if (!sqlDeleteUSSD(idussd)) { + res.send(true, "Не удалось удалить USSD"); + return; + } + + res.send(); +} diff --git a/source/sql.d b/source/sql.d new file mode 100644 index 0000000..a488407 --- /dev/null +++ b/source/sql.d @@ -0,0 +1,454 @@ +module sql; + +import pgdb; +import singlog; +import structures; + +import std.conv; + +/*********************************************************** + Запросы для таблицы номеров телефонов +***********************************************************/ + +GroupDB[] sqlGetListGroups() { + GroupDB[] groups; + try { + auto queryResult = pgsql.sql( + "select distinct + dan.da_group, + dag.da_comment + from da_numbers dan + left join da_groups dag ON dag.da_name = dan.da_group" + ); + foreach (row; queryResult) { + GroupDB data; + + data.name = row["da_group"]; + data.comment = row["da_comment"]; + + groups ~= data; + } + } catch (Exception e) { + log.e("Не удалось выполнить запрос к БД. " ~ e.msg); + } + + return groups; +} + +NumberDB[] sqlGetListNumbers(string group) { + NumberDB[] numbers; + try { + auto queryResult = pgsql.sql( + "select + dan.da_number, + dal.da_comment da_list, + dan.da_all_cc, + dan.da_white_cc, + dan.da_black_cc, + dan.da_comment + from da_numbers dan + left join da_lists dal on dal.da_name = dan.da_list + where dan.da_group = ? + order by dan.da_number", + group + ); + foreach (row; queryResult) { + NumberDB data; + + data.number = row["da_number"]; + data.list = row["da_list"]; + data.all_cc = row["da_all_cc"].to!int; + data.white_cc = row["da_white_cc"].to!int; + data.black_cc = row["da_black_cc"].to!int; + data.comment = row["da_comment"]; + + numbers ~= data; + } + } catch (Exception e) { + log.e("Не удалось выполнить запрос к БД. " ~ e.msg); + } + + return numbers; +} + +NumberDB sqlGetDataNumber(string number) { + NumberDB data; + try { + auto queryResult = pgsql.sql( + "select + da_number, + da_group, + da_list, + da_all_cc, + da_white_cc, + da_black_cc, + da_comment + from da_numbers + where da_number = ?", + number + ); + foreach (row; queryResult) { + data.number = row["da_number"]; + data.group = row["da_group"]; + data.list = row["da_list"]; + data.all_cc = row["da_all_cc"].to!int; + data.white_cc = row["da_white_cc"].to!int; + data.black_cc = row["da_black_cc"].to!int; + data.comment = row["da_comment"]; + } + } catch (Exception e) { + log.e("Не удалось выполнить запрос к БД. " ~ e.msg); + } + + return data; +} + +GroupDB[] sqlGetGroups() { + GroupDB[] groups; + try { + auto queryResult = pgsql.sql( + "select da_name, da_comment from da_groups" + ); + foreach (row; queryResult) { + GroupDB data; + + data.name = row["da_name"]; + data.comment = row["da_comment"]; + + groups ~= data; + } + } catch (Exception e) { + log.e("Не удалось выполнить запрос к БД. " ~ e.msg); + } + + return groups; +} + +ListDB[] sqlGetLists() { + ListDB[] lists; + try { + auto queryResult = pgsql.sql( + "select da_name, da_comment from da_lists" + ); + foreach (row; queryResult) { + ListDB data; + + data.name = row["da_name"]; + data.comment = row["da_comment"]; + + lists ~= data; + } + } catch (Exception e) { + log.e("Не удалось выполнить запрос к БД. " ~ e.msg); + } + + return lists; +} + +bool sqlUpdateNumber(NumberDB number) { + try { + pgsql.sql( + "update da_numbers set + da_group = ?, + da_list = ?, + da_all_cc = ?, + da_white_cc = ?, + da_black_cc = ?, + da_comment = ? + where da_number = ?", + number.group, + number.list, + number.all_cc, + number.white_cc, + number.black_cc, + number.comment, + number.number + ); + } catch (Exception e) { + log.e("Ошибка обновления номера в БД. " ~ e.msg); + return false; + } + return true; +} + +bool sqlDeleteNumber(string number) { + try { + pgsql.sql( + "delete from da_numbers where da_number = ?", number + ); + } catch (Exception e) { + log.e("Ошибка удаления номера в БД. " ~ e.msg); + return false; + } + return true; +} + +bool sqlInsertNumber(NumberDB number) { + try { + pgsql.sql( + "insert into da_numbers + (da_number, da_group, da_list, da_all_cc, da_white_cc, da_black_cc, da_comment) + values + (?, ?, ?, ?, ?, ?, ?)", + number.number, + number.group, + number.list, + number.all_cc, + number.white_cc, + number.black_cc, + number.comment + ); + } catch (Exception e) { + log.error("Ошибка добавления номера телефона в БД. " ~ e.msg); + return false; + } + return true; +} + +/*********************************************************** + Запросы для таблицы SMS +***********************************************************/ + +SMSDB[] sqlGetSMSNumbers() { + SMSDB[] numbers; + try { + auto queryResult = pgsql.sql( + "select distinct da_to from da_sms" + ); + foreach (row; queryResult) { + SMSDB data; + + data.to = row["da_to"]; + + numbers ~= data; + } + } catch (Exception e) { + log.e("Не удалось выполнить запрос к БД. " ~ e.msg); + } + + return numbers; +} + +SMSDB[] sqlGetListSMS(string to) { + SMSDB[] sms; + try { + auto queryResult = pgsql.sql( + "select + da_id, + to_char(da_date, 'YYYY.MM.DD HH24:MI:SS') da_date, + da_to, + da_from, + da_text + from da_sms + where da_to = ? + order by da_date desc", + to + ); + foreach (row; queryResult) { + SMSDB data; + + data.id = row["da_id"].to!int; + data.date = row["da_date"]; + data.to = row["da_to"]; + data.from = row["da_from"]; + data.text = row["da_text"]; + + sms ~= data; + } + } catch (Exception e) { + log.e("Не удалось выполнить запрос к БД. " ~ e.msg); + } + + return sms; +} + +SMSDB sqlGetSMS(int idsms) { + SMSDB data; + try { + auto queryResult = pgsql.sql( + "select + da_id, + to_char(da_date, 'YYYY.MM.DD HH24:MI:SS') da_date, + da_to, + da_from, + da_text + from da_sms + where da_id = ?", + idsms + ); + foreach (row; queryResult) { + data.id = row["da_id"].to!int; + data.date = row["da_date"]; + data.to = row["da_to"]; + data.from = row["da_from"]; + data.text = row["da_text"]; + } + } catch (Exception e) { + log.e("Не удалось выполнить запрос к БД. " ~ e.msg); + } + + return data; +} + +bool sqlDeleteSMS(int idsms) { + try { + pgsql.sql( + "delete from da_sms where da_id = ?", idsms + ); + } catch (Exception e) { + log.e("Ошибка удаления SMS в БД. " ~ e.msg); + return false; + } + return true; +} + +/*********************************************************** + Запросы для таблицы USSD +***********************************************************/ + +USSDDB[] sqlGetUSSDNumbers() { + USSDDB[] numbers; + try { + auto queryResult = pgsql.sql( + "select distinct da_to from da_ussd" + ); + foreach (row; queryResult) { + USSDDB data; + + data.to = row["da_to"]; + + numbers ~= data; + } + } catch (Exception e) { + log.e("Не удалось выполнить запрос к БД. " ~ e.msg); + } + + return numbers; +} + +USSDDB[] sqlGetListUSSD(string to) { + USSDDB[] ussd; + try { + auto queryResult = pgsql.sql( + "select + dau.da_id, + to_char(dau.da_date, 'YYYY.MM.DD HH24:MI:SS') da_date, + dau.da_to, + dau.da_type, + daut.da_comment da_type_comment, + dau.da_text + from da_ussd dau + inner join da_ussd_type daut + on dau.da_type = daut.da_id + where dau.da_to = ? + order by dau.da_date desc", + to + ); + foreach (row; queryResult) { + USSDDB data; + + data.id = row["da_id"].to!int; + data.date = row["da_date"]; + data.to = row["da_to"]; + data.type = row["da_type"].to!int; + data.type_comment = row["da_type_comment"]; + data.text = row["da_text"]; + + ussd ~= data; + } + } catch (Exception e) { + log.e("Не удалось выполнить запрос к БД. " ~ e.msg); + } + + return ussd; +} + +USSDDB sqlGetUSSD(int idussd) { + USSDDB data; + try { + auto queryResult = pgsql.sql( + "select + da_id, + to_char(da_date, 'YYYY.MM.DD HH24:MI:SS') da_date, + da_to, + da_type, + da_text + from da_ussd + where da_id = ?", + idussd + ); + foreach (row; queryResult) { + data.id = row["da_id"].to!int; + data.date = row["da_date"]; + data.to = row["da_to"]; + data.type = row["da_type"].to!int; + data.text = row["da_text"]; + } + } catch (Exception e) { + log.e("Не удалось выполнить запрос к БД. " ~ e.msg); + } + + return data; +} + +bool sqlDeleteUSSD(int idussd) { + try { + pgsql.sql( + "delete from da_ussd where da_id = ?", idussd + ); + } catch (Exception e) { + log.e("Ошибка удаления USSD в БД. " ~ e.msg); + return false; + } + return true; +} + +/*********************************************************** + Запросы для таблицы информации о сервере +***********************************************************/ + +ServerDB sqlGetServerInfo() { + ServerDB server; + try { + auto queryResult = pgsql.sql( + "select + case when da_transparent_mode then 1 else 0 end da_transparent_mode, + da_internal_number, + da_external_number, + case when da_external_number_on then 1 else 0 end da_external_number_on + from da_server + where da_id = 1" + ); + foreach (row; queryResult) { + server.transparent_mode = row["da_transparent_mode"].to!int.to!bool; + server.internal_number = row["da_internal_number"]; + server.external_number = row["da_external_number"]; + server.external_number_on = row["da_external_number_on"].to!int.to!bool; + } + } catch (Exception e) { + log.e("Не удалось выполнить запрос к БД. " ~ e.msg); + } + + return server; +} + +bool sqlUpdateServerInfo(ServerDB server) { + try { + pgsql.sql( + "update da_server set + da_transparent_mode = ?, + da_internal_number = ?, + da_external_number = ?, + da_external_number_on = ? + where da_id = 1", + server.transparent_mode, + server.internal_number, + server.external_number, + server.external_number_on + ); + } catch (Exception e) { + log.e("Ошибка обновления параметров сервера в БД. " ~ e.msg); + return false; + } + return true; +} diff --git a/source/structures.d b/source/structures.d index 1f04a77..b38058d 100644 --- a/source/structures.d +++ b/source/structures.d @@ -4,6 +4,15 @@ struct ServerInfo { string name; } +struct AuthData { + string login; + string password; +} + +struct UserData { + bool login = false; +} + struct WebHost { string[] addresses; ushort http = 0; @@ -22,6 +31,11 @@ struct GroupDB { string comment; } +struct ListDB { + string name; + string comment; +} + struct NumberDB { string number; string group; @@ -31,3 +45,27 @@ struct NumberDB { int black_cc; string comment; } + +struct SMSDB { + int id; + string date; + string to; + string from; + string text; +} + +struct USSDDB { + int id; + string date; + string to; + int type; + string type_comment; + string text; +} + +struct ServerDB { + bool transparent_mode; + string internal_number; + string external_number; + bool external_number_on; +} diff --git a/source/version_.d b/source/version_.d index ea9a3a6..fa80cc9 100644 --- a/source/version_.d +++ b/source/version_.d @@ -1,3 +1,3 @@ module version_; -enum dasterVersion = "v0.0.1"; +enum dasterVersion = "v0.1.0-beta.1"; diff --git a/views/404.dt b/views/404.dt new file mode 100644 index 0000000..0d4d34b --- /dev/null +++ b/views/404.dt @@ -0,0 +1,8 @@ +doctype html +head + title Страница не найдена + link(rel='icon', type='image/png', sizes='128x128', href='favicon.png') + link(rel='stylesheet', type='text/css', href='404.css') +body + div.page + div.pic diff --git a/views/authorization.dt b/views/authorization.dt new file mode 100644 index 0000000..6eb8540 --- /dev/null +++ b/views/authorization.dt @@ -0,0 +1,26 @@ +doctype html +head + title #{serverInfo.name}: авторизация + link(rel='icon', type='image/png', sizes='128x128', href='favicon.png') + link(rel='stylesheet', type='text/css', href='jquery-ui.min.css') + link(rel='stylesheet', type='text/css', href='authorization.css') + script(src='jquery-3.7.0.min.js') + script(src='jquery-ui.min.js') + script(src='noticer.min.js') + script(src='authorization.js') +body + div.form + div.logo + div.title #{serverInfo.name} + table + tbody + tr + td.label Логин: + td + input.input-focus#login(type='text') + tr + td.label Пароль: + td + input.input-focus#password(type='password') + div.div-button + button#authorization Войти diff --git a/views/group-numbers-list.dt b/views/group-numbers-list.dt deleted file mode 100644 index 46d56eb..0000000 --- a/views/group-numbers-list.dt +++ /dev/null @@ -1,22 +0,0 @@ -- import structures; - -table - thead.head - tr - th Номер - th Список - th Общие звонки - th Белые звонки - th Черные звонки - th Комментарий -div.body-rows - table - tbody.body - - foreach (number; listNumbers) - tr.row - td #{number.number} - td #{number.list} - td #{number.all_cc} - td #{number.white_cc} - td #{number.black_cc} - td #{number.comment} diff --git a/views/index.dt b/views/index.dt index 69887bd..60cf645 100644 --- a/views/index.dt +++ b/views/index.dt @@ -6,19 +6,21 @@ head link(rel='stylesheet', type='text/css', href='style.css') script(src='jquery-3.7.0.min.js') script(src='jquery-ui.min.js') - script(src='message.js') + script(src='noticer.min.js') script(src='script.js') body div.div-header - // div.div-add - // button.addNumber Добавить номер - div.div-search - input.input-focus.search(name='search', type='text', value='', placeholder='Найти номер') - // div.div-user Вы вошли как #{user.name} - div.div-user Вы вошли как Александр - div.div-button - button Выход - // button(onclick='logout()') Выход + div.div-add.main-button + button#add-number Добавить номер + div.div-search.main-button + input.input-focus#search(name='search', type='text', value='', placeholder='Найти номер') + div.div-update-tab.main-button + button#update-tab Перезагрузить + div.div-update-tab.main-button + button#update-group Обновить + div.div-user + div.div-logout + button#logout Выход div.content div#tabs ul @@ -32,8 +34,5 @@ body a(href='#tabs-server') Сервер div#tabs-numbers div#tabs-sms - p Список SMS сообщений div#tabs-ussd - p Список результатов USSD запросов div#tabs-server - p Информация о сервере diff --git a/views/list_number_groups.dt b/views/list_number_groups.dt new file mode 100644 index 0000000..9a907a8 --- /dev/null +++ b/views/list_number_groups.dt @@ -0,0 +1,16 @@ +div#accordion-numbers + - foreach (group; listGroups) + h3 #{group.comment} + div.group-content(data-group-name='#{group.name}') + table.table-content + thead.head + tr + th Номер + th Список + th Общие звонки + th Белые звонки + th Черные звонки + th Комментарий + div.body-rows + table.table-content + tbody.body diff --git a/views/list_sms_groups.dt b/views/list_sms_groups.dt new file mode 100644 index 0000000..c4f7270 --- /dev/null +++ b/views/list_sms_groups.dt @@ -0,0 +1,13 @@ +div#accordion-sms + - foreach (number; numbers) + h3 На номер #{number.to} + div.group-content(data-to='#{number.to}') + table.table-content + thead.head + tr + th.sms-content-width Дата + th.sms-content-width От кого + th Текст сообщения + div.body-rows + table.table-content + tbody.body diff --git a/views/list_ussd_groups.dt b/views/list_ussd_groups.dt new file mode 100644 index 0000000..4cd5b44 --- /dev/null +++ b/views/list_ussd_groups.dt @@ -0,0 +1,13 @@ +div#accordion-ussd + - foreach (number; numbers) + h3 На номер #{number.to} + div.group-content(data-to='#{number.to}') + table.table-content + thead.head + tr + th.ussd-content-width Дата + th.ussd-content-width-type Тип + th Текст сообщения + div.body-rows + table.table-content + tbody.body diff --git a/views/number.dt b/views/number.dt new file mode 100644 index 0000000..36efe5e --- /dev/null +++ b/views/number.dt @@ -0,0 +1,42 @@ +div#number-data + table + tbody + tr + td.number-label Номер: + td.number-value + - if (edit) + input.number-input-main.input-focus#number-number(type='text', value='#{dataNumber.number}', disabled) + - else + input.number-input-main.input-focus#number-number(type='text', value='#{dataNumber.number}') + td.number-label Общие звонки: + td.number-value + input.number-input-cc.input-focus#number-all-cc(type='text', value='#{dataNumber.all_cc}') + tr + td.number-label Группа: + td.number-value + select#number-group + - foreach (group; groups) + - if (dataNumber.group == group.name) + option(selected, value='#{group.name}') #{group.comment} + - else + option(value='#{group.name}') #{group.comment} + td.number-label Белые звонки: + td.number-value + input.number-input-cc.input-focus#number-white-cc(type='text', value='#{dataNumber.white_cc}') + tr + td.number-label Список: + td.number-value + select#number-list + - foreach (list; lists) + - if (dataNumber.list == list.name) + option(selected, value='#{list.name}') #{list.comment} + - else + option(value='#{list.name}') #{list.comment} + td.number-label Черные звонки: + td.number-value + input.number-input-cc.input-focus#number-black-cc(type='text', value='#{dataNumber.black_cc}') + div.div-advanced + div.comment + div.comment-name Комментарий + div.comment-content + textarea.input-focus#number-comment #{dataNumber.comment} diff --git a/views/server.dt b/views/server.dt new file mode 100644 index 0000000..f17ecaf --- /dev/null +++ b/views/server.dt @@ -0,0 +1,23 @@ +div#server-data + table + tbody + tr + td.server-label Внутренний номер: + td.server-value + input.input-focus.server-input#server-internal-number(type="text", value="#{dataServer.internal_number}") + td.server-value + tr + td.server-label Внешний номер: + td.server-value + input.input-focus.server-input#server-external-number(type="text", value="#{dataServer.external_number}") + td.server-value + label(for="server-external-number-on") Использовать + input.server-input#server-external-number-on(name="server-external-number-on", type="checkbox", checked="#{dataServer.external_number_on}") + tr + td.server-label Прозрачный режим: + td.server-value + label(for="server-transparent-mode") Включен + input.server-input#server-transparent-mode(name="server-transparent-mode", type="checkbox", checked="#{dataServer.transparent_mode}") + td.server-value +div.server-button + button#server-button Сохранить изменения diff --git a/views/sms.dt b/views/sms.dt new file mode 100644 index 0000000..bc57579 --- /dev/null +++ b/views/sms.dt @@ -0,0 +1,11 @@ +div#sms-data + table + tbody + tr + td.sms-label Дата: + td.sms-value #{dataSMS.date} + tr + td.sms-label.sms-label-text Текст: + td.sms-value + div.sms-content + textarea#sms-content(readonly, data-id="#{dataSMS.id}", data-from="#{dataSMS.from}") #{dataSMS.text} diff --git a/views/ussd.dt b/views/ussd.dt new file mode 100644 index 0000000..8e80a4b --- /dev/null +++ b/views/ussd.dt @@ -0,0 +1,11 @@ +div#ussd-data + table + tbody + tr + td.ussd-label Дата: + td.ussd-value #{dataUSSD.date} + tr + td.ussd-label.ussd-label-text Текст: + td.ussd-value + div.ussd-content + textarea#ussd-content(readonly, data-id="#{dataUSSD.id}") #{dataUSSD.text}