├── public ├── assets │ ├── js │ │ └── app.js │ ├── vendor │ │ ├── leaflet │ │ │ └── images │ │ │ │ ├── layers.png │ │ │ │ ├── layers-2x.png │ │ │ │ ├── marker-icon.png │ │ │ │ ├── marker-shadow.png │ │ │ │ └── marker-icon-2x.png │ │ ├── sweetalert │ │ │ ├── sweetalert.d.ts │ │ │ ├── modules │ │ │ │ ├── markup │ │ │ │ │ ├── content.ts │ │ │ │ │ ├── overlay.ts │ │ │ │ │ ├── modal.ts │ │ │ │ │ ├── buttons.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── icons.ts │ │ │ │ ├── init │ │ │ │ │ ├── overlay.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── text.ts │ │ │ │ │ ├── icon.ts │ │ │ │ │ ├── modal.ts │ │ │ │ │ ├── content.ts │ │ │ │ │ └── buttons.ts │ │ │ │ ├── options │ │ │ │ │ ├── content.ts │ │ │ │ │ ├── deprecations.ts │ │ │ │ │ ├── buttons.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── class-list │ │ │ │ │ └── index.ts │ │ │ │ ├── state.ts │ │ │ │ ├── utils.ts │ │ │ │ ├── actions.ts │ │ │ │ └── event-listeners.ts │ │ │ ├── sweetalert.js │ │ │ ├── css │ │ │ │ ├── icons.css │ │ │ │ ├── content.css │ │ │ │ ├── icons │ │ │ │ │ ├── info.css │ │ │ │ │ ├── warning.css │ │ │ │ │ ├── error.css │ │ │ │ │ └── success.css │ │ │ │ ├── text.css │ │ │ │ ├── button-loader.css │ │ │ │ └── buttons.css │ │ │ ├── core.ts │ │ │ ├── sweetalert.css │ │ │ └── polyfills.js │ │ └── tabler │ │ │ └── static │ │ │ └── illustrations │ │ │ └── undraw_sign_in_e6hj.svg │ └── css │ │ └── app.css └── img │ └── dnms.png ├── util ├── ping.js ├── hash.js ├── dnms │ └── tool.js ├── mikrotik-api.js └── snmp.js ├── views ├── partials │ ├── head.ejs │ ├── javascript.ejs │ ├── footer.ejs │ └── alert-message.ejs ├── auth │ └── login.ejs ├── monitor │ ├── scanner.ejs │ ├── server.ejs │ └── device.ejs ├── report │ └── statistic.ejs ├── tool │ ├── ping.ejs │ ├── arp.ejs │ ├── neighbor.ejs │ ├── host.ejs │ ├── lease.ejs │ └── log.ejs ├── setting │ └── user.ejs └── scheduler │ └── radar.ejs ├── controller ├── manage.js ├── setting.js ├── api │ ├── user.js │ ├── device.js │ ├── subnetwok.js │ └── server.js ├── report.js ├── authentication.js ├── dashboard.js ├── scheduler.js ├── monitor.js └── tool.js ├── example.env ├── config ├── database.js ├── logger.js ├── socket-action.js └── passport.js ├── model ├── extra.js ├── user.js ├── subnetwork.js ├── server.js └── device.js ├── middleware └── app.js ├── LICENSE ├── package.json ├── README.md ├── app.js ├── .gitignore ├── routes.js └── dnms.sql /public/assets/js/app.js: -------------------------------------------------------------------------------- 1 | docReady(function () { 2 | getSiteSelector(); 3 | }); -------------------------------------------------------------------------------- /public/img/dnms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daimus/dnms/HEAD/public/img/dnms.png -------------------------------------------------------------------------------- /public/assets/vendor/leaflet/images/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daimus/dnms/HEAD/public/assets/vendor/leaflet/images/layers.png -------------------------------------------------------------------------------- /public/assets/vendor/leaflet/images/layers-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daimus/dnms/HEAD/public/assets/vendor/leaflet/images/layers-2x.png -------------------------------------------------------------------------------- /public/assets/vendor/leaflet/images/marker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daimus/dnms/HEAD/public/assets/vendor/leaflet/images/marker-icon.png -------------------------------------------------------------------------------- /public/assets/vendor/leaflet/images/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daimus/dnms/HEAD/public/assets/vendor/leaflet/images/marker-shadow.png -------------------------------------------------------------------------------- /public/assets/vendor/leaflet/images/marker-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daimus/dnms/HEAD/public/assets/vendor/leaflet/images/marker-icon-2x.png -------------------------------------------------------------------------------- /util/ping.js: -------------------------------------------------------------------------------- 1 | const ping = require('ping'); 2 | 3 | exports.ping = async (host) => { 4 | let result = await ping.promise.probe(host); 5 | return result; 6 | } -------------------------------------------------------------------------------- /views/partials/head.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/sweetalert.d.ts: -------------------------------------------------------------------------------- 1 | import swal, { SweetAlert } from "./core"; 2 | 3 | declare global { 4 | const swal: SweetAlert; 5 | const sweetAlert: SweetAlert; 6 | } 7 | 8 | export default swal; 9 | export as namespace swal; 10 | -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/modules/markup/content.ts: -------------------------------------------------------------------------------- 1 | import CLASS_NAMES from '../class-list'; 2 | 3 | const { CONTENT } = CLASS_NAMES; 4 | 5 | export const contentMarkup: string = ` 6 |
7 | 8 |
9 | `; 10 | 11 | -------------------------------------------------------------------------------- /public/assets/css/app.css: -------------------------------------------------------------------------------- 1 | .overlay-container { 2 | height: 100%; 3 | width: 100%; 4 | position: absolute;z-index: 1; 5 | top: 0; 6 | left: 0; 7 | background-color: rgb(0,0,0); 8 | background-color: rgba(0,0,0, 0.9); 9 | overflow-x: hidden; 10 | } -------------------------------------------------------------------------------- /controller/manage.js: -------------------------------------------------------------------------------- 1 | exports.server = async (req, res) => { 2 | res.render('manage/server'); 3 | } 4 | 5 | exports.device = (req, res) => { 6 | res.render('manage/device'); 7 | } 8 | 9 | exports.subnetwork = (req, res) => { 10 | res.render('manage/subnetwork'); 11 | } -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/modules/markup/overlay.ts: -------------------------------------------------------------------------------- 1 | import CLASS_NAMES from '../class-list'; 2 | 3 | const { 4 | OVERLAY, 5 | } = CLASS_NAMES; 6 | 7 | const overlay: string = 8 | `
11 |
` 12 | ; 13 | 14 | export default overlay; 15 | -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | APP_NAME=dnms 2 | APP_ENV=development 3 | APP_HOSTNAME=localhost 4 | APP_PORT=3000 5 | 6 | DATABASE_HOST=localhost 7 | DATABASE_USER=root 8 | DATABASE_PASSWORD= 9 | DATABASE_NAME=dnms 10 | DATABASE_DIALECT=mysql 11 | 12 | SESSION_SECRET= 13 | 14 | MAPBOX_TOKEN= 15 | TELEGRAM_TOKEN= 16 | TELEGRAM_CHATID= -------------------------------------------------------------------------------- /controller/setting.js: -------------------------------------------------------------------------------- 1 | const {User} = require('../model/user'); 2 | 3 | exports.user = async (req, res) => { 4 | const userId = req.session.passport.user.id; 5 | const user = await User.findByPk(userId); 6 | res.render('setting/user', { 7 | user: user, 8 | ...req.session.flash 9 | }); 10 | } -------------------------------------------------------------------------------- /config/database.js: -------------------------------------------------------------------------------- 1 | const { Sequelize } = require('sequelize'); 2 | 3 | const sequelize = new Sequelize(process.env.DATABASE_NAME, process.env.DATABASE_USER, process.env.DATABASE_PASSWORD, { 4 | host: process.env.DATABASE_HOST, 5 | dialect: process.env.DATABASE_DIALECT, 6 | logging: false 7 | }); 8 | 9 | exports.sequelize = sequelize; -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/modules/init/overlay.ts: -------------------------------------------------------------------------------- 1 | import { stringToNode } from '../utils'; 2 | import { overlayMarkup } from '../markup'; 3 | 4 | const initOverlayOnce = (): void => { 5 | const overlay = stringToNode(overlayMarkup); 6 | 7 | document.body.appendChild(overlay); 8 | }; 9 | 10 | export default initOverlayOnce; 11 | -------------------------------------------------------------------------------- /model/extra.js: -------------------------------------------------------------------------------- 1 | const { 2 | DataTypes 3 | } = require('sequelize'); 4 | const { 5 | sequelize 6 | } = require('../config/database'); 7 | 8 | const Extra = sequelize.define('extras', { 9 | key: { 10 | type: DataTypes.STRING, 11 | allowNull: false 12 | }, 13 | value: { 14 | type: DataTypes.STRING 15 | } 16 | }, { 17 | timestamps: true 18 | }); 19 | 20 | exports.Extra = Extra; -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/sweetalert.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This makes sure that we can use the global 3 | * swal() function, instead of swal.default() 4 | * See: https://github.com/webpack/webpack/issues/3929 5 | */ 6 | 7 | if (typeof window !== 'undefined') { 8 | require('./sweetalert.css'); 9 | } 10 | 11 | require('./polyfills'); 12 | 13 | var swal = require('./core').default; 14 | 15 | module.exports = swal; 16 | -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/modules/markup/modal.ts: -------------------------------------------------------------------------------- 1 | import CLASS_NAMES from '../class-list'; 2 | 3 | const { 4 | MODAL, 5 | } = CLASS_NAMES; 6 | 7 | export const modalMarkup: string =` 8 | ` 21 | ; 22 | 23 | export default modalMarkup; 24 | 25 | -------------------------------------------------------------------------------- /model/user.js: -------------------------------------------------------------------------------- 1 | const { 2 | DataTypes 3 | } = require('sequelize'); 4 | const { 5 | sequelize 6 | } = require('../config/database'); 7 | 8 | const User = sequelize.define('users', { 9 | name: { 10 | type: DataTypes.STRING, 11 | allowNull: false 12 | }, 13 | username: { 14 | type: DataTypes.STRING, 15 | allowNull: false 16 | }, 17 | password: { 18 | type: DataTypes.STRING, 19 | allowNull: false 20 | } 21 | }, { 22 | timestamps: true 23 | }); 24 | 25 | exports.User = User; -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/modules/markup/buttons.ts: -------------------------------------------------------------------------------- 1 | import CLASS_NAMES from '../class-list'; 2 | 3 | const { 4 | BUTTON_CONTAINER, 5 | BUTTON, 6 | BUTTON_LOADER, 7 | } = CLASS_NAMES; 8 | 9 | export const buttonMarkup: string = ` 10 |
11 | 12 | 15 | 16 |
17 |
18 |
19 |
20 |
21 | 22 |
23 | `; 24 | 25 | -------------------------------------------------------------------------------- /model/subnetwork.js: -------------------------------------------------------------------------------- 1 | const { 2 | DataTypes 3 | } = require('sequelize'); 4 | const { 5 | sequelize 6 | } = require('../config/database'); 7 | 8 | const Subnetwork = sequelize.define('subnetworks', { 9 | serverId: { 10 | type: DataTypes.INTEGER, 11 | allowNull: false 12 | }, 13 | name: { 14 | type: DataTypes.STRING, 15 | allowNull: false 16 | }, 17 | deviceId: { 18 | type: DataTypes.STRING 19 | }, 20 | enabled: { 21 | type: DataTypes.BOOLEAN, 22 | allowNull: false, 23 | defaultValue: true 24 | } 25 | }, { 26 | timestamps: true 27 | }); 28 | 29 | exports.Subnetwork = Subnetwork; -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/css/icons.css: -------------------------------------------------------------------------------- 1 | @import './icons/error'; 2 | @import './icons/warning'; 3 | @import './icons/success'; 4 | @import './icons/info'; 5 | 6 | .swal-icon { 7 | width: 80px; 8 | height: 80px; 9 | border-width: 4px; 10 | border-style: solid; 11 | border-radius: 50%; 12 | padding: 0; 13 | position: relative; 14 | box-sizing: content-box; 15 | margin: 20px auto; 16 | &:first-child { 17 | margin-top: 32px; 18 | } 19 | 20 | &--custom { 21 | width: auto; 22 | height: auto; 23 | max-width: 100%; 24 | border: none; 25 | border-radius: 0; 26 | } 27 | 28 | & img { 29 | max-width: 100%; 30 | max-height: 100%; 31 | } 32 | } 33 | 34 | 35 | -------------------------------------------------------------------------------- /config/logger.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston'); 2 | const logConfiguration = { 3 | level: process.env.APP_ENV !== 'production' ? 'silly' : 'warn', 4 | 'transports': [ 5 | new winston.transports.File({ 6 | filename: 'logs/combined.log' 7 | }), 8 | new winston.transports.File({ 9 | filename: 'logs/error.log', 10 | level: 'error' 11 | }) 12 | ], 13 | format: winston.format.json() 14 | } 15 | 16 | 17 | const logger = winston.createLogger(logConfiguration); 18 | 19 | if (process.env.APP_ENV !== 'production') { 20 | logger.add(new winston.transports.Console({ 21 | format: winston.format.simple(), 22 | })); 23 | } 24 | 25 | module.exports.logger = logger; -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/css/content.css: -------------------------------------------------------------------------------- 1 | .swal-content { 2 | padding: 0 20px; 3 | margin-top: 20px; 4 | font-size: initial; 5 | 6 | &:last-child { 7 | margin-bottom: 20px; 8 | } 9 | 10 | &__input, 11 | &__textarea { 12 | -webkit-appearance: none; 13 | background-color: white; 14 | border: none; 15 | font-size: 14px; 16 | display: block; 17 | box-sizing: border-box; 18 | width: 100%; 19 | border: 1px solid rgba(0, 0, 0, 0.14); 20 | padding: 10px 13px; 21 | border-radius: 2px; 22 | transition: border-color 0.2s; 23 | &:focus { 24 | outline: none; 25 | border-color: #6DB8FF; 26 | } 27 | } 28 | 29 | &__textarea { 30 | resize: vertical; 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/css/icons/info.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --swal-blue: #C9DAE1; 3 | } 4 | 5 | .swal-icon--info { 6 | border-color: var(--swal-blue); 7 | 8 | /* "i"-letter body */ 9 | &::before { 10 | content: ""; 11 | position: absolute; 12 | width: 5px; 13 | height: 29px; 14 | left: 50%; 15 | bottom: 17px; 16 | border-radius: 2px; 17 | margin-left: -2px; 18 | background-color: var(--swal-blue); 19 | } 20 | /* "i"-letter dot */ 21 | &::after { 22 | content: ""; 23 | position: absolute; 24 | width: 7px; 25 | height: 7px; 26 | border-radius: 50%; 27 | margin-left: -3px; 28 | top: 19px; 29 | background-color: var(--swal-blue); 30 | left: 50%; 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /util/hash.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt'); 2 | const {logger} = require('../config/logger'); 3 | 4 | exports.passwordHash = async (password) => { 5 | const hashedPassword = await new Promise((resolve, reject) => { 6 | bcrypt.genSalt(10, (err, salt) => { 7 | if (err) { 8 | logger.error(`gen salt error ${JSON.stringify(err)}`); 9 | reject(err); 10 | } 11 | bcrypt.hash(password, salt, (err, hash) => { 12 | if (err) { 13 | logger.error(`hash error ${JSON.stringify(err)}`); 14 | reject(err); 15 | } 16 | resolve(hash); 17 | }); 18 | }); 19 | }); 20 | return hashedPassword; 21 | } -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/modules/markup/index.ts: -------------------------------------------------------------------------------- 1 | export * from './modal'; 2 | 3 | export { 4 | default as overlayMarkup 5 | } from './overlay'; 6 | 7 | export * from './icons'; 8 | 9 | export * from './content'; 10 | 11 | export * from './buttons'; 12 | 13 | import CLASS_NAMES from '../class-list'; 14 | 15 | const { 16 | MODAL_TITLE, 17 | MODAL_TEXT, 18 | ICON, 19 | FOOTER, 20 | } = CLASS_NAMES; 21 | 22 | export const iconMarkup: string = ` 23 |
` 24 | ; 25 | 26 | export const titleMarkup: string = ` 27 |
28 | `; 29 | 30 | export const textMarkup: string = ` 31 |
` 32 | ; 33 | 34 | export const footerMarkup: string = ` 35 |
36 | `; 37 | 38 | 39 | -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/modules/options/content.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isPlainObject, 3 | } from '../utils'; 4 | 5 | export interface ContentOptions { 6 | element: string|Node, 7 | attributes?: object, 8 | }; 9 | 10 | const defaultInputOptions: ContentOptions = { 11 | element: 'input', 12 | attributes: { 13 | placeholder: "", 14 | }, 15 | }; 16 | 17 | export const getContentOpts = (contentParam: string|object): ContentOptions => { 18 | let opts = {}; 19 | 20 | if (isPlainObject(contentParam)) { 21 | return Object.assign(opts, contentParam); 22 | } 23 | 24 | if (contentParam instanceof Element) { 25 | return { 26 | element: contentParam, 27 | }; 28 | } 29 | 30 | if (contentParam === 'input') { 31 | return defaultInputOptions; 32 | } 33 | 34 | return null; 35 | }; 36 | -------------------------------------------------------------------------------- /middleware/app.js: -------------------------------------------------------------------------------- 1 | const {logger} = require('../config/logger'); 2 | 3 | exports.redirectUnauthenticated = (req, res, next) => { 4 | if (req.isAuthenticated()) { 5 | return next(); 6 | } 7 | logger.error(`user not authenticated`); 8 | res.redirect('/login'); 9 | } 10 | 11 | exports.redirectAuthenticated = (req, res, next) => { 12 | if (req.isAuthenticated()) { 13 | logger.warn("user is allready authenticated"); 14 | return res.redirect('/'); 15 | } 16 | next(); 17 | } 18 | 19 | exports.ensureServerSelected = (req, res, next) => { 20 | const serverId = req.session.serverId; 21 | logger.silly(`selected server id: ${serverId}`); 22 | if (serverId === undefined) { 23 | logger.error(`server id is undefined`); 24 | return res.redirect('/select'); 25 | } 26 | next(); 27 | } -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/css/icons/warning.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --swal-orange: #F8BB86; 3 | } 4 | 5 | .swal-icon--warning { 6 | border-color: var(--swal-orange); 7 | animation: pulseWarning 0.75s infinite alternate; 8 | 9 | /* Exclamation mark */ 10 | &__body { 11 | position: absolute; 12 | width: 5px; 13 | height: 47px; 14 | left: 50%; 15 | top: 10px; 16 | border-radius: 2px; 17 | margin-left: -2px; 18 | background-color: var(--swal-orange); 19 | } 20 | 21 | &__dot { 22 | position: absolute; 23 | width: 7px; 24 | height: 7px; 25 | border-radius: 50%; 26 | margin-left: -4px; 27 | left: 50%; 28 | bottom: -11px; 29 | background-color: var(--swal-orange); 30 | } 31 | } 32 | 33 | @keyframes pulseWarning { 34 | from { 35 | border-color: #F8D486; 36 | } 37 | to { 38 | border-color: var(--swal-orange); 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/modules/init/index.ts: -------------------------------------------------------------------------------- 1 | import { getNode } from '../utils'; 2 | import { SwalOptions } from '../options'; 3 | 4 | import CLASS_NAMES from '../class-list'; 5 | const { MODAL } = CLASS_NAMES; 6 | 7 | import initModalOnce, { 8 | initModalContent, 9 | } from './modal'; 10 | 11 | import initOverlayOnce from './overlay'; 12 | import addEventListeners from '../event-listeners'; 13 | import { throwErr } from '../utils'; 14 | 15 | /* 16 | * Inject modal and overlay into the DOM 17 | * Then format the modal according to the given opts 18 | */ 19 | export const init = (opts: SwalOptions): void => { 20 | const modal: Element = getNode(MODAL); 21 | 22 | if (!modal) { 23 | if (!document.body) { 24 | throwErr("You can only use SweetAlert AFTER the DOM has loaded!"); 25 | } 26 | 27 | initOverlayOnce(); 28 | initModalOnce(); 29 | } 30 | 31 | initModalContent(opts); 32 | addEventListeners(opts); 33 | }; 34 | 35 | export default init; 36 | 37 | -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/css/text.css: -------------------------------------------------------------------------------- 1 | .swal-title { 2 | color: rgba(0, 0, 0, 0.65); 3 | font-weight: 600; 4 | text-transform: none; 5 | position: relative; 6 | display: block; 7 | padding: 13px 16px; 8 | font-size: 27px; 9 | line-height: normal; 10 | text-align: center; 11 | margin-bottom: 0px; 12 | &:first-child { 13 | margin-top: 26px; 14 | } 15 | &:not(:first-child) { 16 | padding-bottom: 0; 17 | } 18 | &:not(:last-child) { 19 | margin-bottom: 13px; 20 | } 21 | } 22 | 23 | .swal-text { 24 | font-size: 16px; 25 | position: relative; 26 | float: none; 27 | line-height: normal; 28 | vertical-align: top; 29 | text-align: left; 30 | display: inline-block; 31 | margin: 0; 32 | padding: 0 10px; 33 | font-weight: 400; 34 | color: rgba(0, 0, 0, 0.64); 35 | max-width: calc(100% - 20px); 36 | overflow-wrap: break-word; 37 | box-sizing: border-box; 38 | &:first-child { 39 | margin-top: 45px; 40 | } 41 | &:last-child { 42 | margin-bottom: 45px; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/modules/class-list/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * List of class names that we 3 | * use throughout the library to 4 | * manipulate the DOM. 5 | */ 6 | 7 | export interface ClassNameList { 8 | [key: string]: string, 9 | }; 10 | 11 | const OVERLAY: string = 'swal-overlay'; 12 | const BUTTON: string = 'swal-button'; 13 | const ICON: string = 'swal-icon'; 14 | 15 | export const CLASS_NAMES: ClassNameList = { 16 | MODAL: 'swal-modal', 17 | OVERLAY, 18 | SHOW_MODAL: `${OVERLAY}--show-modal`, 19 | 20 | MODAL_TITLE: `swal-title`, 21 | MODAL_TEXT: `swal-text`, 22 | ICON, 23 | ICON_CUSTOM: `${ICON}--custom`, 24 | 25 | CONTENT: 'swal-content', 26 | 27 | FOOTER: 'swal-footer', 28 | BUTTON_CONTAINER: 'swal-button-container', 29 | BUTTON, 30 | CONFIRM_BUTTON: `${BUTTON}--confirm`, 31 | CANCEL_BUTTON: `${BUTTON}--cancel`, 32 | DANGER_BUTTON: `${BUTTON}--danger`, 33 | BUTTON_LOADING: `${BUTTON}--loading`, 34 | BUTTON_LOADER: `${BUTTON}__loader`, 35 | }; 36 | 37 | export default CLASS_NAMES; 38 | 39 | -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/modules/markup/icons.ts: -------------------------------------------------------------------------------- 1 | import CLASS_NAMES from '../class-list'; 2 | 3 | const { ICON } = CLASS_NAMES; 4 | 5 | export const errorIconMarkup = (): string => { 6 | const icon = `${ICON}--error`; 7 | const line = `${icon}__line`; 8 | 9 | const markup = ` 10 |
11 | 12 | 13 |
14 | `; 15 | 16 | return markup; 17 | } 18 | 19 | export const warningIconMarkup = (): string => { 20 | const icon = `${ICON}--warning`; 21 | 22 | return ` 23 | 24 | 25 | 26 | `; 27 | }; 28 | 29 | export const successIconMarkup = (): string => { 30 | const icon = `${ICON}--success`; 31 | 32 | return ` 33 | 34 | 35 | 36 |
37 |
38 | `; 39 | }; 40 | -------------------------------------------------------------------------------- /config/socket-action.js: -------------------------------------------------------------------------------- 1 | // const test = require('../util/test'); 2 | const tool = require('../util/dnms/tool'); 3 | const monitor = require('../util/dnms/monitor'); 4 | 5 | const types = { 6 | PING: 'PING' 7 | } 8 | 9 | exports.init = ((socket) => { 10 | socket.on(types.PING, (payload) => { 11 | tool.ping(socket, payload); 12 | }); 13 | socket.on('LOG', () => { 14 | tool.log(socket); 15 | }); 16 | socket.on('PING_SERVER', (payload) => { 17 | monitor.server(socket, payload); 18 | }); 19 | socket.on('MONITOR_DEVICE', (payload) => { 20 | monitor.device(socket, payload); 21 | }); 22 | socket.on('DASHBOARD', (payload) => { 23 | monitor.dashboard(socket, payload); 24 | }); 25 | socket.on('OFFLINE_SCANNER', (payload = { 26 | count: 1 27 | }) => { 28 | monitor.offlineScanner(socket, payload); 29 | }); 30 | socket.on('TRAFFIC', (payload) => { 31 | monitor.traffic(socket, payload); 32 | }); 33 | socket.on('REGISTRATION_TABLE', (payload) => { 34 | monitor.registrationTable(socket, payload); 35 | }) 36 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Daimus Suudi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /model/server.js: -------------------------------------------------------------------------------- 1 | const { 2 | DataTypes 3 | } = require('sequelize'); 4 | const { 5 | sequelize 6 | } = require('../config/database'); 7 | 8 | 9 | const Server = sequelize.define('servers', { 10 | name: { 11 | type: DataTypes.STRING, 12 | allowNull: false 13 | }, 14 | comment: { 15 | type: DataTypes.STRING 16 | }, 17 | connectTo: { 18 | type: DataTypes.STRING, 19 | allowNull: false 20 | }, 21 | apiPort: { 22 | type: DataTypes.INTEGER, 23 | defaultValue: 8278, 24 | allowNull: false 25 | }, 26 | username: { 27 | type: DataTypes.STRING, 28 | defaultValue: 'admin', 29 | allowNull: false 30 | }, 31 | password: { 32 | type: DataTypes.STRING 33 | }, 34 | mainInterface: { 35 | type: DataTypes.STRING, 36 | defaultValue: 'ether1' 37 | }, 38 | latitude: { 39 | type: DataTypes.DOUBLE, 40 | allowNull: true, 41 | defaultValue: 0 42 | }, 43 | longitude: { 44 | type: DataTypes.DOUBLE, 45 | allowNull: true, 46 | defaultValue: 0 47 | }, 48 | enabled: { 49 | type: DataTypes.BOOLEAN, 50 | defaultValue: 1 51 | } 52 | }, { 53 | timestamps: true, 54 | }); 55 | 56 | exports.Server = Server; -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/css/button-loader.css: -------------------------------------------------------------------------------- 1 | .swal-button--loading { 2 | color: transparent; 3 | 4 | & ~ .swal-button__loader { 5 | opacity: 1; 6 | } 7 | } 8 | 9 | .swal-button__loader { 10 | position: absolute; 11 | height: auto; 12 | width: 43px; 13 | z-index: 2; 14 | left: 50%; 15 | top: 50%; 16 | transform: translateX(-50%) translateY(-50%); 17 | text-align: center; 18 | pointer-events: none; 19 | opacity: 0; 20 | 21 | & div { 22 | display: inline-block; 23 | float: none; 24 | vertical-align: baseline; 25 | width: 9px; 26 | height: 9px; 27 | padding: 0; 28 | border: none; 29 | margin: 2px; 30 | opacity: 0.4; 31 | border-radius: 7px; 32 | background-color: rgba(255, 255, 255, 0.9); 33 | transition: background 0.2s; 34 | animation: swal-loading-anim 1s infinite; 35 | 36 | &:nth-child(3n+2) { 37 | animation-delay: 0.15s; 38 | } 39 | 40 | &:nth-child(3n+3) { 41 | animation-delay: 0.3s; 42 | } 43 | } 44 | } 45 | 46 | @keyframes swal-loading-anim { 47 | 0% { opacity: 0.4; } 48 | 20% { opacity: 0.4; } 49 | 50% { opacity: 1.0; } 50 | 100% { opacity: 0.4; } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /controller/api/user.js: -------------------------------------------------------------------------------- 1 | const {User} = require('../../model/user'); 2 | const hash = require('../../util/hash'); 3 | 4 | exports.update = async (req, res) => { 5 | const userId = req.session.passport.user.id; 6 | let password = {}; 7 | if (req.body.password !== ''){ 8 | if (req.body.password !== req.body.password_confirmation){ 9 | req.flash('alert', { 10 | status: 'danger', 11 | title: 'Ouch!', 12 | message: 'Password confirmation not match!' 13 | }); 14 | return res.redirect('/setting/user'); 15 | } 16 | password.password = await hash.passwordHash(req.body.password); 17 | } 18 | await User.update({ 19 | name: req.body.name, 20 | username: req.body.username, 21 | ...password, 22 | }, { 23 | where: { 24 | id: userId 25 | } 26 | }).then(result => { 27 | req.flash('alert', { 28 | status: 'success', 29 | title: 'Success!', 30 | message: 'Update user success!' 31 | }); 32 | }).catch(err => { 33 | logger.error(`error query ${JSON.stringify(error)}`); 34 | }); 35 | res.redirect('/setting/user'); 36 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dnms", 3 | "version": "1.0.0", 4 | "description": "Cross Network Monitoring System", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Muhammad Daimus Suudi", 10 | "license": "ISC", 11 | "dependencies": { 12 | "bcrypt": "^5.0.0", 13 | "body-parser": "^1.19.0", 14 | "change-case": "^4.1.2", 15 | "connect-flash": "^0.1.1", 16 | "connect-mysql": "^4.0.0", 17 | "cookie-parser": "^1.4.5", 18 | "dotenv": "^8.2.0", 19 | "ejs": "^3.1.6", 20 | "express": "^4.17.1", 21 | "express-session": "^1.17.1", 22 | "express-socket.io-session": "^1.3.5", 23 | "flash": "^1.1.0", 24 | "jsend": "^1.1.0", 25 | "mariadb": "^2.5.3", 26 | "method-override": "^3.0.0", 27 | "mysql2": "^2.2.5", 28 | "net-ping": "^1.2.3", 29 | "net-snmp": "^3.5.2", 30 | "node-routeros": "^1.6.9", 31 | "parse-duration": "^0.4.4", 32 | "passport": "^0.4.1", 33 | "passport-local": "^1.0.0", 34 | "path": "^0.12.7", 35 | "ping": "^0.4.0", 36 | "sequelize": "^6.5.0", 37 | "sequelize-cli": "^6.2.0", 38 | "socket.io": "^3.1.2", 39 | "winston": "^3.3.3" 40 | }, 41 | "devDependencies": { 42 | "eslint": "^7.20.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/css/icons/error.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --swal-red: #F27474; 3 | } 4 | 5 | .swal-icon--error { 6 | border-color: var(--swal-red); 7 | animation: animateErrorIcon 0.5s; 8 | 9 | &__x-mark { 10 | position: relative; 11 | display: block; 12 | animation: animateXMark 0.5s; 13 | } 14 | 15 | &__line { 16 | position: absolute; 17 | height: 5px; 18 | width: 47px; 19 | background-color: var(--swal-red); 20 | display: block; 21 | top: 37px; 22 | border-radius: 2px; 23 | 24 | &--left { 25 | transform: rotate(45deg); 26 | left: 17px; 27 | } 28 | 29 | &--right { 30 | transform: rotate(-45deg); 31 | transform: rotate(-45deg); 32 | right: 16px; 33 | } 34 | } 35 | } 36 | 37 | @keyframes animateErrorIcon { 38 | from { 39 | transform: rotateX(100deg); 40 | opacity: 0; 41 | } 42 | to { 43 | transform: rotateX(0deg); 44 | opacity: 1; 45 | } 46 | } 47 | 48 | @keyframes animateXMark { 49 | 0% { 50 | transform: scale(0.4); 51 | margin-top: 26px; 52 | opacity: 0; 53 | } 54 | 50% { 55 | transform: scale(0.4); 56 | margin-top: 26px; 57 | opacity: 0; 58 | } 59 | 80% { 60 | transform: scale(1.15); 61 | margin-top: -6px; 62 | } 63 | 100% { 64 | transform: scale(1); 65 | margin-top: 0; 66 | opacity: 1; 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/modules/init/text.ts: -------------------------------------------------------------------------------- 1 | import { 2 | titleMarkup, 3 | textMarkup, 4 | } from '../markup'; 5 | 6 | import { injectElIntoModal } from './modal'; 7 | 8 | /* 9 | * Fixes a weird bug that doesn't wrap long text in modal 10 | * This is visible in the Safari browser for example. 11 | * https://stackoverflow.com/a/3485654/2679245 12 | */ 13 | const webkitRerender = (el: HTMLElement) => { 14 | if (navigator.userAgent.includes('AppleWebKit')) { 15 | el.style.display = 'none'; 16 | el.offsetHeight; 17 | el.style.display = ''; 18 | } 19 | } 20 | 21 | export const initTitle = (title: string): void => { 22 | if (title) { 23 | const titleEl: HTMLElement = injectElIntoModal(titleMarkup); 24 | titleEl.textContent = title; 25 | 26 | webkitRerender(titleEl); 27 | } 28 | }; 29 | 30 | export const initText = (text: string): void => { 31 | if (text) { 32 | let textNode = document.createDocumentFragment(); 33 | text.split('\n').forEach((textFragment, index, array) => { 34 | textNode.appendChild(document.createTextNode(textFragment)); 35 | 36 | // unless we are on the last element, append a
37 | if (index < array.length - 1) { 38 | textNode.appendChild(document.createElement('br')); 39 | } 40 | }); 41 | const textEl: HTMLElement = injectElIntoModal(textMarkup); 42 | textEl.appendChild(textNode); 43 | 44 | webkitRerender(textEl); 45 | } 46 | }; 47 | 48 | -------------------------------------------------------------------------------- /views/partials/javascript.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /views/partials/footer.ejs: -------------------------------------------------------------------------------- 1 | 31 | 32 |
33 |
34 |
35 |
-------------------------------------------------------------------------------- /model/device.js: -------------------------------------------------------------------------------- 1 | const { 2 | DataTypes 3 | } = require('sequelize'); 4 | const { 5 | sequelize 6 | } = require('../config/database'); 7 | 8 | const Device = sequelize.define('devices', { 9 | serverId: { 10 | type: DataTypes.INTEGER, 11 | allowNull: false 12 | }, 13 | name: { 14 | type: DataTypes.STRING, 15 | allowNull: false 16 | }, 17 | comment: { 18 | type: DataTypes.STRING 19 | }, 20 | connectTo: { 21 | type: DataTypes.STRING, 22 | allowNull: false 23 | }, 24 | snmpCommunity: { 25 | type: DataTypes.STRING, 26 | allowNull: false, 27 | defaultValue: 'public' 28 | }, 29 | os: { 30 | type: DataTypes.STRING, 31 | allowNull: false, 32 | defaultValue: 'other' 33 | }, 34 | wireless: { 35 | type: DataTypes.BOOLEAN, 36 | allowNull: false, 37 | defaultValue: false 38 | }, 39 | latitude: { 40 | type: DataTypes.DOUBLE, 41 | defaultValue: 0 42 | }, 43 | longitude: { 44 | type: DataTypes.DOUBLE, 45 | defaultValue: 0 46 | }, 47 | connectedTo: { 48 | type: DataTypes.INTEGER, 49 | defaultValue: 0 50 | }, 51 | enabled: { 52 | type: DataTypes.BOOLEAN, 53 | defaultValue: true 54 | }, 55 | visible: { 56 | type: DataTypes.BOOLEAN, 57 | defaultValue: true 58 | } 59 | }, { 60 | timestamps: true 61 | }); 62 | 63 | exports.Device = Device; -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | /* global require */ 2 | const passport = require('passport'); 3 | const { 4 | Strategy: LocalStrategy 5 | } = require('passport-local'); 6 | const bcrypt = require('bcrypt'); 7 | 8 | const { 9 | User 10 | } = require('../model/user'); 11 | 12 | passport.serializeUser((user, done) => { 13 | done(null, user); 14 | }); 15 | passport.deserializeUser((user, done) => { 16 | User.findOne({ 17 | where: { 18 | id: user.id 19 | } 20 | }).then((user) => { 21 | done(null, user); 22 | }).catch((error) => { 23 | done(error, null); 24 | }); 25 | }); 26 | 27 | // Local Strategy 28 | passport.use(new LocalStrategy({ 29 | usernameField: 'username', 30 | passwordField: 'password' 31 | }, (username, password, done) => { 32 | User.findOne({ 33 | where: { 34 | username: username.toLocaleLowerCase() 35 | } 36 | }).then((user) => { 37 | if (user === null) { 38 | return done(null, false, { 39 | message: 'User not found' 40 | }); 41 | } 42 | bcrypt.compare(password, user.password, (error, isMatch) => { 43 | if (error) { 44 | return done(error); 45 | } 46 | if (isMatch) { 47 | return done(null, user); 48 | } 49 | return done(null, false, { 50 | message: 'Invalid Password' 51 | }); 52 | }); 53 | }).catch((error) => { 54 | return done(error); 55 | }); 56 | })); -------------------------------------------------------------------------------- /util/dnms/tool.js: -------------------------------------------------------------------------------- 1 | const {Server} = require("../../model/server"); 2 | const mikrotikApi = require("../mikrotik-api"); 3 | const {logger} = require('../../config/logger'); 4 | 5 | exports.ping = async (socket, payload) => { 6 | const serverId = socket.handshake.session.serverId; 7 | const server = await Server.findOne({where: {id: serverId}}); 8 | const conn = { 9 | host: server.connectTo, 10 | user: server.username, 11 | password: server.password, 12 | port: server.apiPort, 13 | }; 14 | const cb = (error, data) => { 15 | if (error) { 16 | logger.error(`cb error ${JSON.stringify(error)}`); 17 | return socket.emit("ALERT", {status: "fail", message: error}); 18 | } 19 | socket.emit("PING", data); 20 | }; 21 | mikrotikApi.ping(conn, payload, cb); 22 | }; 23 | 24 | exports.log = async (socket) => { 25 | const serverId = socket.handshake.session.serverId; 26 | const server = await Server.findOne({where: {id: serverId}}); 27 | const conn = { 28 | host: server.connectTo, 29 | user: server.username, 30 | password: server.password, 31 | port: server.apiPort 32 | }; 33 | const connection = mikrotikApi.createConnection(conn); 34 | connection.connect().then(async () => { 35 | const logs = await mikrotikApi.write(connection, ['/log/print']); 36 | socket.emit('LOG', {logs: logs}); 37 | connection.close(); 38 | }).catch(error => { 39 | return socket.emit("ALERT", { 40 | status: "error", 41 | message: "Error Connection: " + error.toString(), 42 | }); 43 | }) 44 | }; 45 | -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/core.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SweetAlert 3 | * 2014-2017 – Tristan Edwards 4 | * https://github.com/t4t5/sweetalert 5 | */ 6 | 7 | import init from './modules/init'; 8 | 9 | import { 10 | openModal, 11 | onAction, 12 | getState, 13 | stopLoading, 14 | } from './modules/actions'; 15 | 16 | import state, { 17 | setActionValue, 18 | ActionOptions, 19 | SwalState, 20 | } from './modules/state'; 21 | 22 | import { 23 | SwalOptions, 24 | getOpts, 25 | setDefaults, 26 | } from './modules/options'; 27 | 28 | export type SwalParams = (string|Partial)[]; 29 | 30 | export interface SweetAlert { 31 | (...params: SwalParams): Promise, 32 | close? (namespace: string): void, 33 | getState? (): SwalState, 34 | setActionValue? (opts: string|ActionOptions): void, 35 | stopLoading? (): void, 36 | setDefaults? (opts: object): void, 37 | }; 38 | 39 | const swal:SweetAlert = (...args) => { 40 | 41 | // Prevent library to be run in Node env: 42 | if (typeof window === 'undefined') return; 43 | 44 | const opts: SwalOptions = getOpts(...args); 45 | 46 | return new Promise((resolve, reject) => { 47 | state.promise = { resolve, reject }; 48 | 49 | init(opts); 50 | 51 | // For fade animation to work: 52 | setTimeout(() => { 53 | openModal(); 54 | }); 55 | 56 | }); 57 | }; 58 | 59 | swal.close = onAction; 60 | swal.getState = getState; 61 | swal.setActionValue = setActionValue; 62 | swal.stopLoading = stopLoading; 63 | swal.setDefaults = setDefaults; 64 | 65 | export default swal; 66 | 67 | -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/modules/init/icon.ts: -------------------------------------------------------------------------------- 1 | //import { stringToNode } from '../utils'; 2 | import { injectElIntoModal } from './modal'; 3 | 4 | import { 5 | iconMarkup, 6 | errorIconMarkup, 7 | warningIconMarkup, 8 | successIconMarkup, 9 | } from '../markup'; 10 | 11 | import CLASS_NAMES from '../class-list'; 12 | const { ICON, ICON_CUSTOM } = CLASS_NAMES; 13 | 14 | const PREDEFINED_ICONS: string[] = ["error", "warning", "success", "info"]; 15 | 16 | const ICON_CONTENTS: any = { 17 | error: errorIconMarkup(), 18 | warning: warningIconMarkup(), 19 | success: successIconMarkup(), 20 | } 21 | 22 | /* 23 | * Set the warning, error, success or info icons: 24 | */ 25 | const initPredefinedIcon = (type: string, iconEl: Element): void => { 26 | const iconTypeClass: string = `${ICON}--${type}`; 27 | iconEl.classList.add(iconTypeClass); 28 | 29 | const iconContent: string = ICON_CONTENTS[type]; 30 | 31 | if (iconContent) { 32 | iconEl.innerHTML = iconContent; 33 | } 34 | }; 35 | 36 | const initImageURL = (url: string, iconEl: Element): void => { 37 | iconEl.classList.add(ICON_CUSTOM); 38 | 39 | let img = document.createElement('img'); 40 | img.src = url; 41 | 42 | iconEl.appendChild(img); 43 | }; 44 | 45 | const initIcon = (str: string): void => { 46 | if (!str) return; 47 | 48 | let iconEl: Element = injectElIntoModal(iconMarkup); 49 | 50 | if (PREDEFINED_ICONS.includes(str)) { 51 | initPredefinedIcon(str, iconEl); 52 | } else { 53 | initImageURL(str, iconEl); 54 | } 55 | 56 | }; 57 | 58 | export default initIcon; 59 | 60 | -------------------------------------------------------------------------------- /util/mikrotik-api.js: -------------------------------------------------------------------------------- 1 | const RosApi = require('node-routeros').RouterOSAPI; 2 | const {logger} = require('../config/logger'); 3 | 4 | exports.createConnection = (host) => { 5 | const connection = new RosApi(host); 6 | return connection; 7 | } 8 | 9 | exports.write = async (connection, command) => { 10 | return new Promise ((resolve, reject) => { 11 | connection.write(...command).then((data) => { 12 | resolve(data); 13 | }).catch(err => { 14 | logger.error(`snmp error ${JSON.stringify(err)}`); 15 | reject(err); 16 | }) 17 | }); 18 | } 19 | 20 | exports.ping = async (conn, payload, cb) => { 21 | const connection = new RosApi(conn); 22 | connection.connect().then(() => { 23 | let i = 0; 24 | const addressStream = connection.stream(['/ping', '=address='+payload.host], (error, packet) => { 25 | if (!error) { 26 | i++; 27 | if (i === parseInt(payload.count)) { 28 | addressStream.stop().then(() => { 29 | connection.close(); 30 | }).catch((err) => { 31 | logger.error(`mikrotik stream error ${JSON.stringify(err)}`); 32 | cb(err.toString()); 33 | }); 34 | } 35 | } else { 36 | logger.error(`stream error ${JSON.stringify(error)}`); 37 | cb(error); 38 | } 39 | cb(false, packet); 40 | }); 41 | }).catch(error => { 42 | logger.error(`mikrotik connection error ${JSON.stringify(error)}`); 43 | cb(`${error.name}: ${error.message}`); 44 | }); 45 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DNMS 2 | 3 | Network monitoring app build build with NodeJS. Compatible with device with RouterOS and AirOS. 4 | 5 | **Feature** 6 | 1. Monitoring latency, signal strength, traffic, etc. 7 | 2. Informative display with map, diagram, table 8 | 3. Grouping device 9 | 4. Notification 10 | 5. Multi server 11 | 6. Additional tools for diagnostic like ping, log, ARP, etc. 12 | 13 | **Requirements** 14 | 1. SNMP enabled 15 | 2. MikroTik API enabled 16 | 3. NodeJS ^14 17 | 4. MySQL ^5.6 18 | 19 | ## Instalation 20 | - Clone this repo 21 | ```sh 22 | $ git clone https://github.com/daimus/dnms.git 23 | ``` 24 | - Import `dnms.sql` to your database 25 | - CD to app directory and install required package 26 | ```sh 27 | $ npm install 28 | ``` 29 | - Run app 30 | ```sh 31 | $ node app.js 32 | ``` 33 | > #### Default Authentication 34 | > - **Username:** admin 35 | > - **Password:** 11111111 36 | 37 | ## Screenshoots 38 | 39 | I can't provide live preview of this app. But here some screenshoots to give you an insight about this app. 40 | 41 | ![Screenshot](https://1.bp.blogspot.com/-toUGHq34tzA/YJzrpZaVIuI/AAAAAAAADU4/FpkeurL9ThctwDZF33sPpA5hbQ-b2PDmQCLcBGAsYHQ/s16000/dashboard.jpg) 42 | ![Screenshot](https://1.bp.blogspot.com/-qB-lCxOI7OQ/YJzrpYkyPGI/AAAAAAAADU0/6JguPbVKr8I_DJZr1qvWPCV_EpvDTMSZgCLcBGAsYHQ/s16000/diagram.jpg) 43 | ![Screenshot](https://1.bp.blogspot.com/-nXIOWjXUUqM/YJzrpZR9hvI/AAAAAAAADU8/3ci70mFzE8gZrL12uLjBtuRrjLbaW8e9gCLcBGAsYHQ/s16000/manage.jpg) 44 | ![Screenshot](https://1.bp.blogspot.com/-SAgy4ySaUUo/YJzrp0qQfxI/AAAAAAAADVA/fTWRJFgTECQAtB11Ccs1LimBKTD6OVNiACLcBGAsYHQ/s16000/map.jpg) 45 | ![Screenshot](https://1.bp.blogspot.com/-TEUBewBloe4/YJzrqG95EjI/AAAAAAAADVE/V9MNjq4DwQcIM5e3RDbaXXPBL1u7M6q3gCLcBGAsYHQ/s16000/monitor.jpg) 46 | 47 | -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/modules/state.ts: -------------------------------------------------------------------------------- 1 | import { CONFIRM_KEY } from './options/buttons'; 2 | 3 | export interface SwalState { 4 | isOpen: boolean, 5 | promise: { 6 | resolve?(value: string): void, 7 | reject?(): void, 8 | }, 9 | actions: { 10 | [namespace: string]: { 11 | value?: string | any, 12 | closeModal?: boolean 13 | }, 14 | }, 15 | timer: number, 16 | }; 17 | 18 | export interface ActionOptions { 19 | [buttonNamespace: string]: { 20 | value?: string, 21 | closeModal?: boolean 22 | }, 23 | }; 24 | 25 | const defaultState: SwalState = { 26 | isOpen: false, 27 | promise: null, 28 | actions: {}, 29 | timer: null, 30 | }; 31 | 32 | let state: SwalState = Object.assign({}, defaultState); 33 | 34 | export const resetState = (): void => { 35 | state = Object.assign({}, defaultState); 36 | } 37 | 38 | /* 39 | * Change what the promise resolves to when the user clicks the button. 40 | * This is called internally when using { input: true } for example. 41 | */ 42 | export const setActionValue = (opts: string|ActionOptions) => { 43 | 44 | if (typeof opts === "string") { 45 | return setActionValueForButton(CONFIRM_KEY, opts); 46 | } 47 | 48 | for (let namespace in opts) { 49 | setActionValueForButton(namespace, opts[namespace]); 50 | } 51 | }; 52 | 53 | const setActionValueForButton = (namespace: string, value: string | any) => { 54 | if (!state.actions[namespace]) { 55 | state.actions[namespace] = {}; 56 | } 57 | 58 | Object.assign(state.actions[namespace], { 59 | value, 60 | }); 61 | }; 62 | 63 | /* 64 | * Sets other button options, e.g. 65 | * whether the button should close the modal or not 66 | */ 67 | export const setActionOptionsFor = (buttonKey: string, { 68 | closeModal = true, 69 | } = {}) => { 70 | Object.assign(state.actions[buttonKey], { 71 | closeModal, 72 | }); 73 | }; 74 | 75 | export default state; 76 | 77 | -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/modules/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Get a DOM element from a class name: 3 | */ 4 | export const getNode = (className: string): HTMLElement => { 5 | const selector = `.${className}`; 6 | 7 | return document.querySelector(selector); 8 | }; 9 | 10 | export const stringToNode = (html: string): HTMLElement => { 11 | let wrapper: HTMLElement = document.createElement('div'); 12 | wrapper.innerHTML = html.trim(); 13 | 14 | return wrapper.firstChild; 15 | }; 16 | 17 | export const insertAfter = (newNode: Node, referenceNode: Node) => { 18 | let nextNode = referenceNode.nextSibling; 19 | let parentNode = referenceNode.parentNode; 20 | 21 | parentNode.insertBefore(newNode, nextNode); 22 | }; 23 | 24 | export const removeNode = (node: Node) => { 25 | node.parentElement.removeChild(node); 26 | }; 27 | 28 | export const throwErr = (message: string) => { 29 | // Remove multiple spaces: 30 | message = message.replace(/ +(?= )/g,''); 31 | message = message.trim(); 32 | 33 | throw `SweetAlert: ${message}`; 34 | }; 35 | 36 | /* 37 | * Match plain objects ({}) but NOT null 38 | */ 39 | export const isPlainObject = (value: any): boolean => { 40 | if (Object.prototype.toString.call(value) !== '[object Object]') { 41 | return false; 42 | } else { 43 | var prototype = Object.getPrototypeOf(value); 44 | return prototype === null || prototype === Object.prototype; 45 | } 46 | }; 47 | 48 | /* 49 | * Take a number and return a version with ordinal suffix 50 | * Example: 1 => 1st 51 | */ 52 | export const ordinalSuffixOf = (num: number): string => { 53 | let j = num % 10; 54 | let k = num % 100; 55 | 56 | if (j === 1 && k !== 11) { 57 | return `${num}st`; 58 | } 59 | 60 | if (j === 2 && k !== 12) { 61 | return `${num}nd`; 62 | } 63 | 64 | if (j === 3 && k !== 13) { 65 | return `${num}rd`; 66 | } 67 | 68 | return `${num}th`; 69 | }; 70 | 71 | -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/modules/actions.ts: -------------------------------------------------------------------------------- 1 | import { getNode } from './utils'; 2 | import { CANCEL_KEY } from './options/buttons'; 3 | 4 | import CLASS_NAMES from './class-list'; 5 | 6 | const { 7 | OVERLAY, 8 | SHOW_MODAL, 9 | BUTTON, 10 | BUTTON_LOADING, 11 | } = CLASS_NAMES; 12 | 13 | import state, { SwalState } from './state'; 14 | 15 | export const openModal = (): void => { 16 | let overlay = getNode(OVERLAY); 17 | overlay.classList.add(SHOW_MODAL); 18 | 19 | state.isOpen = true; 20 | }; 21 | 22 | const hideModal = (): void => { 23 | let overlay = getNode(OVERLAY); 24 | overlay.classList.remove(SHOW_MODAL); 25 | 26 | state.isOpen = false; 27 | }; 28 | 29 | /* 30 | * Triggers when the user presses any button, or 31 | * hits Enter inside the input: 32 | */ 33 | export const onAction = (namespace: string = CANCEL_KEY): void => { 34 | const { value, closeModal } = state.actions[namespace]; 35 | 36 | if (closeModal === false) { 37 | const buttonClass = `${BUTTON}--${namespace}`; 38 | const button = getNode(buttonClass); 39 | button.classList.add(BUTTON_LOADING); 40 | } else { 41 | hideModal(); 42 | } 43 | 44 | state.promise.resolve(value); 45 | }; 46 | 47 | /* 48 | * Filter the state object. Remove the stuff 49 | * that's only for internal use 50 | */ 51 | export const getState = (): SwalState => { 52 | const publicState = Object.assign({}, state); 53 | delete publicState.promise; 54 | delete publicState.timer; 55 | 56 | return publicState; 57 | }; 58 | 59 | /* 60 | * Stop showing loading animation on button 61 | * (to display error message in input for example) 62 | */ 63 | export const stopLoading = (): void => { 64 | const buttons: NodeListOf = document.querySelectorAll(`.${BUTTON}`); 65 | 66 | for (let i = 0; i < buttons.length; i++) { 67 | const button: Element = buttons[i]; 68 | button.classList.remove(BUTTON_LOADING); 69 | } 70 | }; 71 | 72 | 73 | -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/sweetalert.css: -------------------------------------------------------------------------------- 1 | @import './css/icons'; 2 | @import './css/text'; 3 | @import './css/buttons'; 4 | @import './css/content'; 5 | @import './css/button-loader'; 6 | 7 | :root { 8 | --swal-modal-width: 478px; 9 | } 10 | 11 | .swal-overlay { 12 | position: fixed; 13 | top: 0; 14 | bottom: 0; 15 | left: 0; 16 | right: 0; 17 | text-align: center; 18 | font-size: 0; /* Remove gap between inline-block elements */ 19 | overflow-y: auto; 20 | 21 | background-color: rgba(0, 0, 0, 0.4); 22 | z-index: 10000; 23 | pointer-events: none; 24 | opacity: 0; 25 | transition: opacity 0.3s; 26 | &::before { 27 | content: ' '; 28 | display: inline-block; 29 | vertical-align: middle; /* vertical alignment of the inline element */ 30 | height: 100%; 31 | } 32 | 33 | &--show-modal { 34 | opacity: 1; 35 | pointer-events: auto; 36 | 37 | & .swal-modal { 38 | opacity: 1; 39 | pointer-events: auto; 40 | box-sizing: border-box; 41 | animation: showSweetAlert 0.3s; 42 | will-change: transform; 43 | } 44 | } 45 | } 46 | 47 | .swal-modal { 48 | width: var(--swal-modal-width); 49 | opacity: 0; 50 | pointer-events: none; 51 | background-color: white; 52 | text-align: center; 53 | border-radius: 5px; 54 | 55 | position: static; 56 | margin: 20px auto; 57 | display: inline-block; 58 | vertical-align: middle; 59 | 60 | transform: scale(1); 61 | transform-origin: 50% 50%; 62 | z-index: 10001; 63 | transition: transform 0.3s, opacity 0.2s; 64 | 65 | @media all and (max-width: 500px) { 66 | width: calc(100% - 20px); 67 | } 68 | } 69 | 70 | @keyframes showSweetAlert { 71 | 0% { 72 | transform: scale(1); 73 | } 74 | 1% { 75 | transform: scale(0.5); 76 | } 77 | 78 | 45% { 79 | transform: scale(1.05); 80 | } 81 | 82 | 80% { 83 | transform: scale(0.95); 84 | } 85 | 86 | 100% { 87 | transform: scale(1); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /controller/report.js: -------------------------------------------------------------------------------- 1 | const mikrotikApi = require("../util/mikrotik-api"); 2 | const { 3 | Server 4 | } = require("../model/server"); 5 | const { 6 | logger 7 | } = require('../config/logger'); 8 | 9 | exports.statistic = async (req, res) => { 10 | const serverId = req.session.serverId; 11 | let usageStat = {}; 12 | const server = await Server.findOne({ 13 | where: { 14 | id: serverId, 15 | }, 16 | }); 17 | const conn = { 18 | host: server.connectTo, 19 | user: server.username, 20 | password: server.password, 21 | port: server.apiPort, 22 | keepalive: true, 23 | }; 24 | const connection = mikrotikApi.createConnection(conn); 25 | await connection.connect().then(async () => { 26 | const queues = await mikrotikApi.write(connection, [ 27 | "/queue/simple/print", 28 | ]); 29 | connection.close(); 30 | let data = Array(); 31 | queues.map((queue) => { 32 | if (!queue["name"].includes("X_") && !queue["name"].includes("hs-")) { 33 | const maxLimit = queue["max-limit"].split("/"); 34 | const bytes = queue["bytes"].split("/"); 35 | data.push({ 36 | name: queue["name"], 37 | target: queue["target"], 38 | maxTxLimit: parseInt(maxLimit[0]), 39 | maxRxLimit: parseInt(maxLimit[1]), 40 | txBytes: parseInt(bytes[0]), 41 | rxBytes: parseInt(bytes[1]), 42 | totalBytes: parseInt(bytes[0]) + parseInt(bytes[1]), 43 | }); 44 | } 45 | 46 | data.sort(function (a, b) { 47 | return b.totalBytes - a.totalBytes; 48 | }); 49 | }); 50 | usageStat = data; 51 | }).catch((error) => { 52 | logger.error(`mikrotik connection error ${JSON.stringify(error)}`); 53 | req.flash('alert', { 54 | title: 'Server Unreachable!', 55 | message: '', 56 | status: 'error' 57 | }); 58 | }); 59 | res.render("report/statistic", { 60 | usageStat: usageStat, 61 | server: server, 62 | alert: req.flash('alert') 63 | }); 64 | }; -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/modules/init/modal.ts: -------------------------------------------------------------------------------- 1 | import { ButtonList } from './../options/buttons'; 2 | import { stringToNode, getNode } from '../utils'; 3 | import { modalMarkup } from '../markup'; 4 | import { SwalOptions } from '../options'; 5 | 6 | import CLASS_NAMES from '../class-list'; 7 | const { MODAL, OVERLAY } = CLASS_NAMES; 8 | 9 | import initIcon from './icon'; 10 | import { initTitle, initText } from './text'; 11 | import initButtons from './buttons'; 12 | import initContent from './content'; 13 | 14 | export const injectElIntoModal = (markup: string): HTMLElement => { 15 | const modal: Element = getNode(MODAL); 16 | const el: HTMLElement = stringToNode(markup); 17 | 18 | modal.appendChild(el); 19 | 20 | return el; 21 | }; 22 | 23 | /* 24 | * Remove eventual added classes + 25 | * reset all content inside: 26 | */ 27 | const resetModalElement = (modal: Element): void => { 28 | modal.className = MODAL; 29 | modal.textContent = ''; 30 | }; 31 | 32 | /* 33 | * Add custom class to modal element 34 | */ 35 | const customizeModalElement = (modal: Element, opts: SwalOptions): void => { 36 | resetModalElement(modal); 37 | 38 | const { className } = opts; 39 | 40 | if (className) { 41 | modal.classList.add(className); 42 | } 43 | }; 44 | 45 | /* 46 | * It's important to run the following functions in this particular order, 47 | * so that the elements get appended one after the other. 48 | */ 49 | export const initModalContent = (opts: SwalOptions): void => { 50 | // Start from scratch: 51 | const modal: Element = getNode(MODAL); 52 | customizeModalElement(modal, opts); 53 | 54 | initIcon(opts.icon); 55 | initTitle(opts.title); 56 | initText(opts.text); 57 | initContent(opts.content); 58 | initButtons(opts.buttons as ButtonList, opts.dangerMode); 59 | }; 60 | 61 | const initModalOnce = (): void => { 62 | const overlay: Element = getNode(OVERLAY); 63 | const modal = stringToNode(modalMarkup); 64 | 65 | overlay.appendChild(modal); 66 | }; 67 | 68 | export default initModalOnce; 69 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const expressSession = require('express-session'); 3 | const flash = require('connect-flash'); 4 | const dotenv = require('dotenv').config(); 5 | const methodOverride = require('method-override'); 6 | const jsend = require('jsend'); 7 | const cookieParser = require('cookie-parser'); 8 | const passport = require('passport'); 9 | const passportConfig = require('./config/passport'); 10 | const {logger} = require('./config/logger'); 11 | const sharedSession = require('express-socket.io-session'); 12 | const MySQLStore = require('connect-mysql')(expressSession); 13 | 14 | const app = express(); 15 | 16 | const httpServer = require("http").createServer(app); 17 | const io = require("socket.io")(httpServer); 18 | 19 | app.use(express.json()); 20 | app.use(express.urlencoded({ 21 | extended: true 22 | })); 23 | app.use(express.static('public')); 24 | app.set('view engine', 'ejs'); 25 | app.use(jsend.middleware); 26 | app.use(methodOverride('_method')); 27 | app.use(cookieParser(process.env.SESSION_SECRET)); 28 | const session = expressSession({ 29 | resave: true, 30 | saveUninitialized: true, 31 | secret: process.env.SESSION_SECRET, 32 | cookie: { 33 | maxAge: 2678400000 34 | }, 35 | store: new MySQLStore({ 36 | config: { 37 | user: process.env.DATABASE_USER, 38 | password: process.env.DATABASE_PASSWORD, 39 | database: process.env.DATABASE_NAME 40 | } 41 | }) 42 | }); 43 | app.use(session); 44 | app.use(flash()); 45 | app.use((req, res, next) => { 46 | res.locals.alert = req.flash('alert'); 47 | next(); 48 | }); 49 | app.use(passport.initialize()); 50 | app.use(passport.session()); 51 | 52 | const routes = require('./routes'); 53 | routes(app); 54 | 55 | io.use(sharedSession(session)); 56 | io.on("connection", (socket) => { 57 | const socketAction = require('./config/socket-action'); 58 | socketAction.init(socket); 59 | logger.silly(`incoming socket connection: ${socket.id}`); 60 | socket.on('disconnect', () => { 61 | logger.silly(`socket connection terminated: ${socket.id}`); 62 | }); 63 | }); 64 | 65 | httpServer.listen(process.env.APP_PORT, () => { 66 | logger.info(`server is running`); 67 | }); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # MacOS unnecessary file 107 | .DS_Store 108 | 109 | # App 110 | /logs 111 | .env 112 | -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/modules/init/content.ts: -------------------------------------------------------------------------------- 1 | import { ContentOptions } from '../options/content'; 2 | import { CONFIRM_KEY } from '../options/buttons'; 3 | import { injectElIntoModal } from './modal'; 4 | import { contentMarkup } from '../markup'; 5 | import { setActionValue } from '../state'; 6 | import { onAction } from '../actions'; 7 | 8 | import CLASS_NAMES from '../class-list'; 9 | const { CONTENT } = CLASS_NAMES; 10 | 11 | /* 12 | * Add an to the content container. 13 | * Update the "promised" value of the confirm button whenever 14 | * the user types into the input (+ make it "" by default) 15 | * Set the default focus on the input. 16 | */ 17 | const addInputEvents = (input: HTMLElement): void => { 18 | 19 | input.addEventListener('input', (e) => { 20 | const target = e.target as HTMLInputElement; 21 | const text = target.value; 22 | setActionValue(text); 23 | }); 24 | 25 | input.addEventListener('keyup', (e) => { 26 | if (e.key === "Enter") { 27 | return onAction(CONFIRM_KEY); 28 | } 29 | }); 30 | 31 | /* 32 | * FIXME (this is a bit hacky) 33 | * We're overwriting the default value of confirm button, 34 | * as well as overwriting the default focus on the button 35 | */ 36 | setTimeout(() => { 37 | input.focus(); 38 | setActionValue(''); 39 | }, 0); 40 | 41 | }; 42 | 43 | const initPredefinedContent = (content: Node, elName: string, attrs: any): void => { 44 | const el: HTMLElement = document.createElement(elName); 45 | 46 | const elClass = `${CONTENT}__${elName}`; 47 | el.classList.add(elClass); 48 | 49 | // Set things like "placeholder": 50 | for (let key in attrs) { 51 | let value: string = attrs[key]; 52 | 53 | (el)[key] = value; 54 | } 55 | 56 | if (elName === "input") { 57 | addInputEvents(el); 58 | } 59 | 60 | content.appendChild(el); 61 | }; 62 | 63 | const initContent = (opts: ContentOptions): void => { 64 | if (!opts) return; 65 | 66 | const content: Node = injectElIntoModal(contentMarkup); 67 | 68 | const { element, attributes } = opts; 69 | 70 | if (typeof element === "string") { 71 | initPredefinedContent(content, element, attributes); 72 | } else { 73 | content.appendChild(element); 74 | } 75 | }; 76 | 77 | export default initContent; 78 | 79 | -------------------------------------------------------------------------------- /controller/authentication.js: -------------------------------------------------------------------------------- 1 | const passport = require("passport"); 2 | const {User} = require("../model/user"); 3 | 4 | exports.login = (req, res) => { 5 | res.render('auth/login'); 6 | } 7 | 8 | exports.signin = (req, res, next) => { 9 | passport.authenticate('local', (err, user, info) => { 10 | if (err){ 11 | logger.error(`user authenticate failed ${JSON.stringify(err)}`); 12 | return next(err); 13 | } 14 | if (!user){ 15 | req.flash('alert', { 16 | status: 'error', 17 | message: info.message 18 | }); 19 | return res.redirect('/login'); 20 | } 21 | req.logIn(user, (err) => { 22 | if (err){ 23 | logger.error(`login failed ${JSON.stringify(err)}`); 24 | return next(err); 25 | } 26 | res.redirect('/'); 27 | }); 28 | })(req, res, next); 29 | } 30 | 31 | exports.register = (req, res) => { 32 | res.render('auth/register'); 33 | } 34 | 35 | exports.signup = (req, res, next) => { 36 | const user = new User(req.body); 37 | 38 | User.findOne({username: req.body.username}, (err, existingUser) => { 39 | if (err) { 40 | return next(err); 41 | } 42 | if (existingUser){ 43 | req.flash('alert', {message: `${user.email} already registered. Please login with correct credentials`, status: 'danger', title: 'Ouch!'}) 44 | return res.redirect('/login'); 45 | } 46 | user.save((err) => { 47 | if (err){ 48 | logger.error(`failed to save user ${JSON.stringify(err)}`); 49 | return next(err); 50 | } 51 | req.logIn(user, (err) => { 52 | if (err){ 53 | logger.error(`login failed ${JSON.stringify(err)}`); 54 | return next(err); 55 | } 56 | res.redirect('/'); 57 | }); 58 | }); 59 | }); 60 | } 61 | 62 | exports.logout = (req, res) => { 63 | req.logout(); 64 | req.session.destroy((err) => { 65 | if (err){ 66 | logger.error(`failed to destroy session ${JSON.stringify(err)}`); 67 | } 68 | req.user = null; 69 | res.redirect('/login'); 70 | }); 71 | } -------------------------------------------------------------------------------- /controller/dashboard.js: -------------------------------------------------------------------------------- 1 | const {logger} = require('../config/logger'); 2 | const { 3 | Server 4 | } = require('../model/server'); 5 | const mikrotikApi = require('../util/mikrotik-api'); 6 | 7 | exports.index = async (req, res) => { 8 | const server = await Server.findOne({ 9 | where: { 10 | id: req.session.serverId 11 | }, 12 | raw: true 13 | }); 14 | let interfaces = Array(); 15 | const conn = { 16 | host: server.connectTo, 17 | user: server.username, 18 | password: server.password, 19 | port: server.apiPort 20 | }; 21 | console.log('conn :>> ', conn); 22 | const connection = mikrotikApi.createConnection(conn); 23 | await connection.connect().then(async () => { 24 | interfaces = await mikrotikApi.write(connection, ['/interface/print']); 25 | connection.close(); 26 | }).catch((err) => { 27 | logger.error(`mikrotik connection error ${JSON.stringify(err)}`); 28 | req.flash('alert', { 29 | title: 'Server Unreachable!', 30 | status: 'error', 31 | message: '', 32 | }); 33 | }); 34 | res.render('dashboard', { 35 | server: server, 36 | interfaces: interfaces, 37 | alert: req.flash('alert') 38 | }); 39 | } 40 | 41 | exports.selectServer = async (req, res) => { 42 | let action = 'NOTHING'; 43 | const servers = await Server.findAll(); 44 | let serverCount = servers.length; 45 | let enabledServerCount = 0; 46 | servers.map(server => { 47 | if (server.enabled) enabledServerCount++; 48 | }); 49 | if (enabledServerCount === 1) { 50 | servers.map(server => { 51 | if (server.enabled){ 52 | req.session.serverId = server.id; 53 | }; 54 | }); 55 | return res.redirect('/'); 56 | } 57 | if (serverCount === 0) { 58 | action = 'CREATE'; 59 | } 60 | if (serverCount > 1 && enabledServerCount === 0) { 61 | action = 'ENABLE'; 62 | } 63 | if (serverCount > 1 && enabledServerCount > 1) { 64 | action = 'SELECT'; 65 | } 66 | res.render('select-server', { 67 | servers: servers, 68 | action: action 69 | }); 70 | } 71 | 72 | const sleep = (ms) => { 73 | return new Promise(resolve => setTimeout(resolve, ms)); 74 | } -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/css/buttons.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --swal-btn-confirm: #7cd1f9; 3 | --swal-btn-confirm-hover: color(var(--swal-btn-confirm) shade(3%)); 4 | --swal-btn-confirm-active: color(var(--swal-btn-confirm) shade(10%)); 5 | 6 | --swal-btn-cancel: #EFEFEF; 7 | --swal-btn-cancel-hover: color(var(--swal-btn-cancel) shade(3%)); 8 | --swal-btn-cancel-active: color(var(--swal-btn-cancel) shade(10%)); 9 | 10 | --swal-btn-danger: #e64942; 11 | --swal-btn-danger-hover: color(var(--swal-btn-danger) shade(3%)); 12 | --swal-btn-danger-active: color(var(--swal-btn-danger) shade(10%)); 13 | 14 | --swal-focus-color: rgba(43, 114, 165, 0.3); 15 | } 16 | 17 | .swal-footer { 18 | text-align: right; 19 | padding-top: 13px; 20 | margin-top: 13px; 21 | padding: 13px 16px; 22 | border-radius: inherit; 23 | border-top-left-radius: 0; 24 | border-top-right-radius: 0; 25 | } 26 | 27 | .swal-button-container { 28 | margin: 5px; 29 | display: inline-block; 30 | position: relative; 31 | } 32 | 33 | .swal-button { 34 | background-color: var(--swal-btn-confirm); 35 | color: white; 36 | border: none; 37 | box-shadow: none; 38 | border-radius: 5px; 39 | font-weight: 600; 40 | font-size: 14px; 41 | padding: 10px 24px; 42 | margin: 0; 43 | cursor: pointer; 44 | &[not:disabled]:hover { 45 | background-color: var(--swal-btn-confirm-hover); 46 | } 47 | &:active { 48 | background-color: var(--swal-btn-confirm-active); 49 | } 50 | &:focus { 51 | outline: none; 52 | box-shadow: 53 | 0px 0px 0px 1px white, 54 | 0px 0px 0px 3px rgba(43, 114, 165, 0.29); 55 | } 56 | &[disabled] { 57 | opacity: 0.5; 58 | cursor: default; 59 | } 60 | /* Remove ugly dotted lines in FireFox: */ 61 | &::-moz-focus-inner { 62 | border: 0; 63 | } 64 | 65 | &--cancel { 66 | color: #555555; 67 | background-color: var(--swal-btn-cancel); 68 | &[not:disabled]:hover { 69 | background-color: var(--swal-btn-cancel-hover); 70 | } 71 | &:active { 72 | background-color: var(--swal-btn-cancel-active); 73 | } 74 | &:focus { 75 | box-shadow: 76 | 0px 0px 0px 1px white, 77 | 0px 0px 0px 3px rgba(116, 136, 150, 0.29); 78 | } 79 | } 80 | 81 | &--danger { 82 | background-color: var(--swal-btn-danger); 83 | &[not:disabled]:hover { 84 | background-color: var(--swal-btn-danger-hover); 85 | } 86 | &:active { 87 | background-color: var(--swal-btn-danger-active); 88 | } 89 | &:focus { 90 | box-shadow: 91 | 0px 0px 0px 1px white, 92 | 0px 0px 0px 3px rgba(165, 43, 43, 0.29); 93 | } 94 | } 95 | } 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/modules/init/buttons.ts: -------------------------------------------------------------------------------- 1 | import { stringToNode } from '../utils'; 2 | import { injectElIntoModal } from './modal'; 3 | 4 | import CLASS_NAMES from '../class-list'; 5 | const { BUTTON, DANGER_BUTTON } = CLASS_NAMES; 6 | 7 | import { ButtonList, ButtonOptions, CONFIRM_KEY } from '../options/buttons'; 8 | import { footerMarkup, buttonMarkup } from '../markup'; 9 | 10 | import { onAction } from '../actions'; 11 | import { 12 | setActionValue, 13 | setActionOptionsFor, 14 | ActionOptions, 15 | } from '../state'; 16 | 17 | /* 18 | * Generate a button, with a container element, 19 | * the right class names, the text, and an event listener. 20 | * IMPORTANT: This will also add the button's action, which can be triggered even if the button element itself isn't added to the modal. 21 | */ 22 | const getButton = (namespace: string, { 23 | text, 24 | value, 25 | className, 26 | closeModal, 27 | }: ButtonOptions, dangerMode: boolean): Node => { 28 | const buttonContainer: any = stringToNode(buttonMarkup); 29 | 30 | const buttonEl: HTMLElement = buttonContainer.querySelector(`.${BUTTON}`); 31 | 32 | const btnNamespaceClass = `${BUTTON}--${namespace}`; 33 | buttonEl.classList.add(btnNamespaceClass); 34 | 35 | if (className) { 36 | const classNameArray = Array.isArray(className) 37 | ? className 38 | : className.split(' '); 39 | classNameArray 40 | .filter(name => name.length > 0) 41 | .forEach(name => { 42 | buttonEl.classList.add(name); 43 | }); 44 | } 45 | 46 | if (dangerMode && namespace === CONFIRM_KEY) { 47 | buttonEl.classList.add(DANGER_BUTTON); 48 | } 49 | 50 | buttonEl.textContent = text; 51 | 52 | let actionValues: ActionOptions = {}; 53 | actionValues[namespace] = value; 54 | setActionValue(actionValues); 55 | 56 | setActionOptionsFor(namespace, { 57 | closeModal, 58 | }); 59 | 60 | buttonEl.addEventListener('click', () => { 61 | return onAction(namespace); 62 | }); 63 | 64 | return buttonContainer; 65 | }; 66 | 67 | /* 68 | * Create the buttons-container, 69 | * then loop through the ButtonList object 70 | * and append every button to it. 71 | */ 72 | const initButtons = (buttons: ButtonList, dangerMode: boolean): void => { 73 | 74 | const footerEl: Element = injectElIntoModal(footerMarkup); 75 | 76 | for (let key in buttons) { 77 | const buttonOpts: ButtonOptions = buttons[key] as ButtonOptions; 78 | const buttonEl: Node = getButton(key, buttonOpts, dangerMode); 79 | 80 | if (buttonOpts.visible) { 81 | footerEl.appendChild(buttonEl); 82 | } 83 | } 84 | 85 | /* 86 | * If the footer has no buttons, there's no 87 | * point in keeping it: 88 | */ 89 | if (footerEl.children.length === 0) { 90 | footerEl.remove(); 91 | } 92 | }; 93 | 94 | export default initButtons; 95 | -------------------------------------------------------------------------------- /controller/api/device.js: -------------------------------------------------------------------------------- 1 | const { 2 | Device 3 | } = require('../../model/device'); 4 | const { 5 | Op 6 | } = require('sequelize'); 7 | const { logger } = require('../../config/logger'); 8 | 9 | exports.get = async (req, res) => { 10 | let paging = {}; 11 | if (parseInt(req.query.size) > 0) { 12 | const limit = parseInt(req.query.size); 13 | const offset = parseInt(req.query.page) ? (parseInt(req.query.page) - 1) * limit : 0; 14 | paging = { 15 | offset: offset, 16 | limit: limit 17 | } 18 | } 19 | const keyword = req.query.keyword ? req.query.keyword : ''; 20 | await Device.findAndCountAll({ 21 | where: { 22 | [Op.or]: [{ 23 | name: { 24 | [Op.like]: `%${keyword}%` 25 | } 26 | }, { 27 | comment: { 28 | [Op.like]: `%${keyword}%` 29 | } 30 | }], 31 | serverId: req.session.serverId 32 | }, 33 | ...paging 34 | }).then(result => { 35 | res.jsend.success({ 36 | count: result.count, 37 | rows: result.rows 38 | }) 39 | }).catch(error => { 40 | logger.error(`error query ${JSON.stringify(error)}`); 41 | }); 42 | } 43 | 44 | exports.create = async (req, res) => { 45 | const data = req.body 46 | data.latitude = parseFloat(req.body.latitude) ? parseFloat(req.body.latitude) : 0; 47 | data.longitude = parseFloat(req.body.longitude) ? parseFloat(req.body.longitude) : 0; 48 | data.wireless = req.body.wireless ? true : false; 49 | data.serverId = req.session.serverId; 50 | await Device.create(data).then(result => { 51 | res.jsend.success(result); 52 | }).catch(error => { 53 | logger.error(`error query ${JSON.stringify(error)}`); 54 | res.jsend.fail(error); 55 | }); 56 | } 57 | 58 | exports.update = async (req, res) => { 59 | const data = req.body 60 | data.latitude = parseFloat(req.body.latitude) ? parseFloat(req.body.latitude) : 0; 61 | data.longitude = parseFloat(req.body.longitude) ? parseFloat(req.body.longitude) : 0; 62 | data.wireless = req.body.wireless ? true : false; 63 | await Device.update(data, { 64 | where: { 65 | id: req.params.deviceId 66 | } 67 | }).then(result => { 68 | res.jsend.success(result); 69 | }).catch(error => { 70 | logger.error(`error query ${JSON.stringify(error)}`); 71 | res.jsend.fail(error); 72 | }); 73 | } 74 | 75 | exports.destroy = async (req, res) => { 76 | await Device.destroy({ 77 | where: { 78 | id: req.params.deviceId 79 | } 80 | }).then(result => { 81 | res.jsend.success(result); 82 | }).catch(error => { 83 | logger.error(`error query ${JSON.stringify(error)}`); 84 | res.jsend.fail(error); 85 | }); 86 | } -------------------------------------------------------------------------------- /views/auth/login.ejs: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Sign in - DNMS 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 |

Login to your account

27 |
28 | <%- include('../partials/alert-message') %> 29 |
30 |
31 | 32 | 33 |
34 |
35 | 36 |
37 | 39 | 40 | 43 | 44 | 45 | 47 | 48 | 49 | 50 |
51 |
52 | 55 |
56 |
57 |
58 | Don't have account yet? Sign up 59 |
60 |
61 |
62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/modules/options/deprecations.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A list of all the deprecated options from SweetAlert 1.X 3 | * These should log a warning telling users how to upgrade. 4 | */ 5 | 6 | export const logDeprecation = (name: string): void => { 7 | const details: OptionReplacement = DEPRECATED_OPTS[name]; 8 | const { onlyRename, replacement, subOption, link } = details; 9 | 10 | const destiny = (onlyRename) ? 'renamed' : 'deprecated'; 11 | 12 | let message = `SweetAlert warning: "${name}" option has been ${destiny}.`; 13 | 14 | if (replacement) { 15 | const subOptionText = (subOption) ? ` "${subOption}" in ` : ' '; 16 | message += ` Please use${subOptionText}"${replacement}" instead.`; 17 | } 18 | 19 | const DOMAIN = 'https://sweetalert.js.org'; 20 | 21 | if (link) { 22 | message += ` More details: ${DOMAIN}${link}`; 23 | } else { 24 | message += ` More details: ${DOMAIN}/guides/#upgrading-from-1x`; 25 | } 26 | 27 | console.warn(message); 28 | }; 29 | 30 | export interface OptionReplacement { 31 | replacement?: string, 32 | onlyRename?: boolean, 33 | subOption?: string, 34 | link?: string, 35 | }; 36 | 37 | export interface OptionReplacementsList { 38 | [name: string]: OptionReplacement, 39 | }; 40 | 41 | export const DEPRECATED_OPTS: OptionReplacementsList = { 42 | 'type': { 43 | replacement: 'icon', 44 | link: '/docs/#icon', 45 | }, 46 | 'imageUrl': { 47 | replacement: 'icon', 48 | link: '/docs/#icon', 49 | }, 50 | 'customClass': { 51 | replacement: 'className', 52 | onlyRename: true, 53 | link: '/docs/#classname', 54 | }, 55 | 'imageSize': {}, 56 | 'showCancelButton': { 57 | replacement: 'buttons', 58 | link: '/docs/#buttons', 59 | }, 60 | 'showConfirmButton': { 61 | replacement: 'button', 62 | link: '/docs/#button', 63 | }, 64 | 'confirmButtonText': { 65 | replacement: 'button', 66 | link: '/docs/#button', 67 | }, 68 | 'confirmButtonColor': {}, 69 | 'cancelButtonText': { 70 | replacement: 'buttons', 71 | link: '/docs/#buttons', 72 | }, 73 | 'closeOnConfirm': { 74 | replacement: 'button', 75 | subOption: 'closeModal', 76 | link: '/docs/#button', 77 | }, 78 | 'closeOnCancel': { 79 | replacement: 'buttons', 80 | subOption: 'closeModal', 81 | link: '/docs/#buttons', 82 | }, 83 | 'showLoaderOnConfirm': { 84 | replacement: 'buttons', 85 | }, 86 | 'animation': {}, 87 | 'inputType': { 88 | replacement: 'content', 89 | link: '/docs/#content', 90 | }, 91 | 'inputValue': { 92 | replacement: 'content', 93 | link: '/docs/#content', 94 | }, 95 | 'inputPlaceholder': { 96 | replacement: 'content', 97 | link: '/docs/#content', 98 | }, 99 | 'html': { 100 | replacement: 'content', 101 | link: '/docs/#content', 102 | }, 103 | 'allowEscapeKey': { 104 | replacement: 'closeOnEsc', 105 | onlyRename: true, 106 | link: '/docs/#closeonesc', 107 | }, 108 | 'allowClickOutside': { 109 | replacement: 'closeOnClickOutside', 110 | onlyRename: true, 111 | link: '/docs/#closeonclickoutside', 112 | }, 113 | }; 114 | -------------------------------------------------------------------------------- /controller/api/subnetwok.js: -------------------------------------------------------------------------------- 1 | const { 2 | Subnetwork 3 | } = require('../../model/subnetwork'); 4 | const { 5 | Device 6 | } = require('../../model/device'); 7 | const { 8 | Op 9 | } = require('sequelize'); 10 | const {logger} = require('../../config/logger'); 11 | 12 | exports.get = async (req, res) => { 13 | let paging = {}; 14 | if (parseInt(req.query.size) > 0) { 15 | const limit = parseInt(req.query.size); 16 | const offset = parseInt(req.query.page) ? (parseInt(req.query.page) - 1) * limit : 0; 17 | paging = { 18 | offset: offset, 19 | limit: limit 20 | } 21 | } 22 | const keyword = req.query.keyword ? req.query.keyword : ''; 23 | await Subnetwork.findAndCountAll({ 24 | where: { 25 | [Op.or]: [{ 26 | name: { 27 | [Op.like]: `%${keyword}%` 28 | } 29 | }], 30 | serverId: req.session.serverId 31 | }, 32 | ...paging 33 | }).then(async (result) => { 34 | const count = result.count; 35 | const rows = result.rows; 36 | for (const row of rows) { 37 | row.dataValues.member = await Device.findAll({ 38 | where: { 39 | id: { 40 | [Op.or]: JSON.parse(row.deviceId) 41 | }, 42 | serverId: req.session.serverId 43 | } 44 | }); 45 | } 46 | 47 | res.jsend.success({ 48 | count: count, 49 | rows: rows 50 | }) 51 | }).catch(error => { 52 | logger.error(`error query ${JSON.stringify(error)}`); 53 | }); 54 | } 55 | 56 | exports.create = async (req, res) => { 57 | const data = { 58 | serverId: req.session.serverId, 59 | name: req.body.name, 60 | deviceId: JSON.stringify(req.body.deviceId), 61 | enabled: req.body.enabled 62 | } 63 | await Subnetwork.create(data).then(result => { 64 | res.jsend.success(result); 65 | }).catch(error => { 66 | logger.error(`error query ${JSON.stringify(error)}`); 67 | res.jsend.fail(error); 68 | }); 69 | } 70 | 71 | exports.update = async (req, res) => { 72 | const data = { 73 | serverId: req.session.serverId, 74 | name: req.body.name, 75 | deviceId: JSON.stringify(req.body.deviceId), 76 | enabled: req.body.enabled 77 | } 78 | await Subnetwork.update(data, { 79 | where: { 80 | id: req.params.subnetworkId 81 | } 82 | }).then(result => { 83 | res.jsend.success(result); 84 | }).catch(error => { 85 | logger.error(`error query ${JSON.stringify(error)}`); 86 | res.jsend.fail(error); 87 | }); 88 | } 89 | 90 | exports.destroy = async (req, res) => { 91 | await Subnetwork.destroy({ 92 | where: { 93 | id: req.params.subnetworkId 94 | } 95 | }).then(result => { 96 | res.jsend.success(result); 97 | }).catch(error => { 98 | logger.error(`error query ${JSON.stringify(error)}`); 99 | res.jsend.fail(error); 100 | }); 101 | } -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/css/icons/success.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --swal-green: #A5DC86; 3 | --swal-green-light: rgba(165, 220, 134, 0.2); 4 | } 5 | 6 | .swal-icon--success { 7 | border-color: var(--swal-green); 8 | 9 | /* Moving circular line */ 10 | &::before, 11 | &::after { 12 | content: ''; 13 | border-radius: 50%; 14 | position: absolute; 15 | width: 60px; 16 | height: 120px; 17 | background: white; 18 | transform: rotate(45deg); 19 | } 20 | 21 | &::before { 22 | border-radius: 120px 0 0 120px; 23 | top: -7px; 24 | left: -33px; 25 | transform: rotate(-45deg); 26 | transform-origin: 60px 60px; 27 | } 28 | 29 | &::after { 30 | border-radius: 0 120px 120px 0; 31 | top: -11px; 32 | left: 30px; 33 | transform: rotate(-45deg); 34 | transform-origin: 0px 60px; 35 | animation: rotatePlaceholder 4.25s ease-in; 36 | } 37 | 38 | /* Ring */ 39 | &__ring { 40 | width: 80px; 41 | height: 80px; 42 | border: 4px solid var(--swal-green-light); 43 | border-radius: 50%; 44 | box-sizing: content-box; 45 | position: absolute; 46 | left: -4px; 47 | top: -4px; 48 | z-index: 2; 49 | } 50 | 51 | /* Hide corners left from animation */ 52 | &__hide-corners { 53 | width: 5px; 54 | height: 90px; 55 | background-color: white; 56 | padding: 1px; 57 | position: absolute; 58 | left: 28px; 59 | top: 8px; 60 | z-index: 1; 61 | transform: rotate(-45deg); 62 | } 63 | 64 | &__line { 65 | height: 5px; 66 | background-color: var(--swal-green); 67 | display: block; 68 | border-radius: 2px; 69 | position: absolute; 70 | z-index: 2; 71 | 72 | &--tip { 73 | width: 25px; 74 | left: 14px; 75 | top: 46px; 76 | transform: rotate(45deg); 77 | animation: animateSuccessTip 0.75s; 78 | } 79 | &--long { 80 | width: 47px; 81 | right: 8px; 82 | top: 38px; 83 | transform: rotate(-45deg); 84 | animation: animateSuccessLong 0.75s; 85 | } 86 | } 87 | } 88 | 89 | @keyframes rotatePlaceholder { 90 | 0% { 91 | transform: rotate(-45deg); 92 | } 93 | 5% { 94 | transform: rotate(-45deg); 95 | } 96 | 12% { 97 | transform: rotate(-405deg); 98 | } 99 | 100% { 100 | transform: rotate(-405deg); 101 | } 102 | } 103 | 104 | @keyframes animateSuccessTip { 105 | 0% { 106 | width: 0; 107 | left: 1px; 108 | top: 19px; 109 | } 110 | 54% { 111 | width: 0; 112 | left: 1px; 113 | top: 19px; 114 | } 115 | 70% { 116 | width: 50px; 117 | left: -8px; 118 | top: 37px; 119 | } 120 | 84% { 121 | width: 17px; 122 | left: 21px; 123 | top: 48px; 124 | } 125 | 100% { 126 | width: 25px; 127 | left: 14px; 128 | top: 45px; 129 | } 130 | } 131 | 132 | @keyframes animateSuccessLong { 133 | 0% { 134 | width: 0; 135 | right: 46px; 136 | top: 54px; 137 | } 138 | 65% { 139 | width: 0; 140 | right: 46px; 141 | top: 54px; 142 | } 143 | 84% { 144 | width: 55px; 145 | right: 0px; 146 | top: 35px; 147 | } 148 | 100% { 149 | width: 47px; 150 | right: 8px; 151 | top: 38px; 152 | } 153 | } 154 | 155 | -------------------------------------------------------------------------------- /controller/scheduler.js: -------------------------------------------------------------------------------- 1 | const { 2 | Device 3 | } = require('../model/device'); 4 | const { 5 | Extra 6 | } = require('../model/extra'); 7 | const {Server} = require('../model/server'); 8 | const { 9 | Op 10 | } = require("sequelize"); 11 | const { logger } = require('../config/logger'); 12 | 13 | exports.radar = async (req, res) => { 14 | const devices = await Device.findAll(); 15 | const radarTelegramReport = await Extra.findOne({ 16 | where: { 17 | key: 'radarTelegramReport' 18 | }, 19 | raw: true 20 | }); 21 | res.render('scheduler/radar', { 22 | devices: devices, 23 | radarTelegramReport: radarTelegramReport 24 | }); 25 | } 26 | 27 | exports.configureRadar = async (req, res) => { 28 | if (req.body.invisible !== undefined){ 29 | await Device.update({ 30 | visible: true 31 | }, { 32 | where: { 33 | visible: false 34 | } 35 | }); 36 | await Device.update({ 37 | visible: false 38 | }, { 39 | where: { 40 | id: { 41 | [Op.in]: req.body.invisible 42 | } 43 | } 44 | }); 45 | } 46 | await Extra.update({ 47 | value: req.body['radar-telegram-report'] ? 1 : 0 48 | }, { 49 | where: { 50 | key: 'radarTelegramReport' 51 | } 52 | }); 53 | res.redirect('/scheduler/radar'); 54 | } 55 | 56 | exports.runRadar = async (req, res) => { 57 | const monitor = require('../util/dnms/monitor'); 58 | const servers = await Server.findAll({ 59 | where: { 60 | enabled: true 61 | } 62 | }); 63 | const radarTelegramReport = await Extra.findOne({ 64 | where: { 65 | key: 'radarTelegramReport' 66 | }, 67 | raw: true 68 | }); 69 | let totalOffline = 0; 70 | let message = 'OFFLINE REPORT %0A'; 71 | for (const server of servers) { 72 | const devices = await Device.findAll({ 73 | where: { 74 | serverId: server.id, 75 | visible: true 76 | }, 77 | raw: true 78 | }); 79 | const offlineDevice = await monitor.offlineScanner(null, { 80 | devices: devices, 81 | return: true, 82 | count: 1 83 | }); 84 | totalOffline += offlineDevice.length; 85 | if (offlineDevice.length > 0) { 86 | message += `%0A ${server.name} - ${server.comment}: %0A` 87 | for (let i = 0; i < offlineDevice.length; i++) { 88 | message += `${i + 1}. ${offlineDevice[i].name} %0A`; 89 | } 90 | } 91 | } 92 | 93 | if (totalOffline > 0 && radarTelegramReport.value === '1') { 94 | const https = require('https'); 95 | const options = new URL(`https://api.telegram.org/bot${process.env.TELEGRAM_TOKEN}/sendMessage?chat_id=${process.env.TELEGRAM_CHATID}&text=${message}`); 96 | 97 | const request = https.get(options, (response) => { 98 | logger.silly('send webhook telegram success'); 99 | }); 100 | request.on('error', (e) => { 101 | logger.error(`error send webhook ${JSON.stringify(e)}`); 102 | }) 103 | } 104 | 105 | res.jsend.success(true); 106 | } -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/polyfills.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Promise polyfill 3 | */ 4 | var Promise = require('promise-polyfill'); 5 | 6 | if (typeof window !== 'undefined' && !window.Promise) { 7 | window.Promise = Promise; 8 | } 9 | 10 | /** 11 | * Object.assign() polyfill 12 | */ 13 | require('es6-object-assign/auto'); 14 | 15 | /** 16 | * String.prototype.includes() polyfill 17 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes#Polyfill 18 | */ 19 | if (!String.prototype.includes) { 20 | String.prototype.includes = function(search, start) { 21 | 'use strict'; 22 | if (typeof start !== 'number') { 23 | start = 0; 24 | } 25 | 26 | if (start + search.length > this.length) { 27 | return false; 28 | } else { 29 | return this.indexOf(search, start) !== -1; 30 | } 31 | }; 32 | } 33 | 34 | /** 35 | * Array.prototype.includes() polyfill 36 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes#Polyfill 37 | */ 38 | if (!Array.prototype.includes) { 39 | Object.defineProperty(Array.prototype, 'includes', { 40 | value: function(searchElement, fromIndex) { 41 | 42 | // 1. Let O be ? ToObject(this value). 43 | if (this == null) { 44 | throw new TypeError('"this" is null or not defined'); 45 | } 46 | 47 | var o = Object(this); 48 | 49 | // 2. Let len be ? ToLength(? Get(O, "length")). 50 | var len = o.length >>> 0; 51 | 52 | // 3. If len is 0, return false. 53 | if (len === 0) { 54 | return false; 55 | } 56 | 57 | // 4. Let n be ? ToInteger(fromIndex). 58 | // (If fromIndex is undefined, this step produces the value 0.) 59 | var n = fromIndex | 0; 60 | 61 | // 5. If n ≥ 0, then 62 | // a. Let k be n. 63 | // 6. Else n < 0, 64 | // a. Let k be len + n. 65 | // b. If k < 0, let k be 0. 66 | var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0); 67 | 68 | function sameValueZero(x, y) { 69 | return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y)); 70 | } 71 | 72 | // 7. Repeat, while k < len 73 | while (k < len) { 74 | // a. Let elementK be the result of ? Get(O, ! ToString(k)). 75 | // b. If SameValueZero(searchElement, elementK) is true, return true. 76 | // c. Increase k by 1. 77 | if (sameValueZero(o[k], searchElement)) { 78 | return true; 79 | } 80 | k++; 81 | } 82 | 83 | // 8. Return false 84 | return false; 85 | } 86 | }); 87 | } 88 | 89 | /** 90 | * ChildNode.remove() polyfill 91 | * @see https://github.com/jserz/js_piece/blob/master/DOM/ChildNode/remove()/remove().md 92 | */ 93 | 94 | if (typeof window !== 'undefined') { 95 | (function (arr) { 96 | arr.forEach(function (item) { 97 | if (item.hasOwnProperty('remove')) { 98 | return; 99 | } 100 | Object.defineProperty(item, 'remove', { 101 | configurable: true, 102 | enumerable: true, 103 | writable: true, 104 | value: function remove() { 105 | this.parentNode.removeChild(this); 106 | } 107 | }); 108 | }); 109 | })([Element.prototype, CharacterData.prototype, DocumentType.prototype]); 110 | } 111 | -------------------------------------------------------------------------------- /controller/api/server.js: -------------------------------------------------------------------------------- 1 | const { 2 | Server 3 | } = require('../../model/server'); 4 | const { 5 | Op 6 | } = require('sequelize'); 7 | const {logger} = require('../../config/logger'); 8 | 9 | exports.get = async (req, res) => { 10 | let paging = {}; 11 | if (parseInt(req.query.size) > 0) { 12 | const limit = parseInt(req.query.size); 13 | const offset = parseInt(req.query.page) ? (parseInt(req.query.page) - 1) * limit : 0; 14 | paging = { 15 | offset: offset, 16 | limit: limit 17 | } 18 | } 19 | const keyword = req.query.keyword ? req.query.keyword : ''; 20 | await Server.findAndCountAll({ 21 | where: { 22 | [Op.or]: [{ 23 | name: { 24 | [Op.like]: `%${keyword}%` 25 | } 26 | }, { 27 | comment: { 28 | [Op.like]: `%${keyword}%` 29 | } 30 | }] 31 | }, 32 | raw: true, 33 | ...paging 34 | }).then(result => { 35 | const currentServerId = req.session.serverId; 36 | if (currentServerId !== undefined) { 37 | result.rows.forEach((value) => { 38 | if (value.id == currentServerId) { 39 | value.selected = true; 40 | } 41 | }); 42 | } else { 43 | result.rows[0].selected = true; 44 | req.session.serverId = result.rows[0].id; 45 | } 46 | res.jsend.success({ 47 | count: result.count, 48 | rows: result.rows 49 | }) 50 | }).catch(error => { 51 | logger.error(`error query ${JSON.stringify(error)}`); 52 | }); 53 | } 54 | 55 | exports.selectSite = (req, res) => { 56 | logger.info(`server changed to ${req.body.serverId}`); 57 | req.session.serverId = req.body.serverId; 58 | res.jsend.success(true); 59 | } 60 | 61 | exports.create = async (req, res) => { 62 | const data = req.body; 63 | data.latitude = parseFloat(req.body.latitude) ? parseFloat(req.body.latitude) : 0; 64 | data.longitude = parseFloat(req.body.longitude) ? parseFloat(req.body.longitude) : 0, 65 | await Server.create(data).then(result => { 66 | res.jsend.success(result); 67 | }).catch(error => { 68 | logger.error(`error query ${JSON.stringify(error)}`); 69 | res.jsend.fail(error); 70 | }); 71 | } 72 | 73 | exports.update = async (req, res) => { 74 | const data = req.body; 75 | data.latitude = parseFloat(req.body.latitude) ? parseFloat(req.body.latitude) : 0; 76 | data.longitude = parseFloat(req.body.longitude) ? parseFloat(req.body.longitude) : 0, 77 | await Server.update(data, { 78 | where: { 79 | id: req.params.serverId 80 | } 81 | }).then(result => { 82 | res.jsend.success(result); 83 | }).catch(error => { 84 | logger.error(`error query ${JSON.stringify(error)}`); 85 | res.jsend.fail(error); 86 | }); 87 | } 88 | 89 | exports.destroy = async (req, res) => { 90 | if (req.params.serverId === req.session.serverId){ 91 | return res.jsend.error('Cannot delete active server!'); 92 | } 93 | await Server.destroy({ 94 | where: { 95 | id: req.params.serverId 96 | } 97 | }).then(result => { 98 | res.jsend.success(result); 99 | }).catch(error => { 100 | logger.error(`error query ${JSON.stringify(error)}`); 101 | res.jsend.fail(error); 102 | }); 103 | } -------------------------------------------------------------------------------- /routes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | const { 5 | redirectUnauthenticated, 6 | redirectAuthenticated, 7 | ensureServerSelected 8 | } = require('./middleware/app'); 9 | 10 | const dashboard = require('./controller/dashboard'); 11 | const manage = require('./controller/manage'); 12 | const authentication = require('./controller/authentication'); 13 | const tool = require('./controller/tool'); 14 | const monitor = require('./controller/monitor'); 15 | const report = require('./controller/report'); 16 | const setting = require('./controller/setting'); 17 | const scheduler = require('./controller/scheduler'); 18 | 19 | // API CONTROLLER 20 | const server = require('./controller/api/server'); 21 | const device = require('./controller/api/device'); 22 | const subnetwork = require('./controller/api/subnetwok'); 23 | const user = require('./controller/api/user'); 24 | 25 | let routes = (app) => { 26 | // AUTHENTICATION 27 | router.get('/login', redirectAuthenticated, authentication.login); 28 | router.post('/login', redirectAuthenticated, authentication.signin); 29 | router.get('/logout', authentication.logout); 30 | 31 | 32 | const RAS = [redirectUnauthenticated, ensureServerSelected]; 33 | // MANAGE 34 | router.get('/', RAS, dashboard.index); 35 | router.get('/select', dashboard.selectServer); 36 | router.get('/manage/server', RAS, manage.server); 37 | router.get('/manage/device', RAS, manage.device); 38 | router.get('/manage/subnetwork', RAS, manage.subnetwork); 39 | 40 | // MONITOR 41 | router.get('/monitor/server', RAS, monitor.server); 42 | router.get('/monitor/device', RAS, monitor.device); 43 | router.get('/monitor/device/:deviceId', RAS, monitor.detail); 44 | router.get('/monitor/diagram', RAS, monitor.diagram); 45 | router.get('/monitor/scanner', RAS, monitor.scanner); 46 | router.get('/monitor/map', RAS, monitor.map); 47 | 48 | // REPORT 49 | router.get('/report/statistic', RAS, report.statistic); 50 | 51 | // TOOL 52 | router.get('/tool/ping', RAS, tool.ping); 53 | router.get('/tool/log', RAS, tool.log); 54 | router.get('/tool/arp', RAS, tool.arp); 55 | router.get('/tool/neighbor', RAS, tool.neighbor); 56 | router.get('/tool/lease', RAS, tool.lease); 57 | router.get('/tool/host', RAS, tool.host); 58 | 59 | // SETTING 60 | router.get('/setting/user', RAS, setting.user); 61 | 62 | // CRON / SCHEDULER 63 | router.get('/scheduler/radar', RAS, scheduler.radar); 64 | router.patch('/scheduler/radar/configure', RAS, scheduler.configureRadar); 65 | router.get('/scheduler/radar/run', scheduler.runRadar); 66 | 67 | // API ROUTES 68 | router.get('/api/server', RAS, server.get); 69 | router.post('/api/server/selectSite', server.selectSite); 70 | router.post('/api/server/create', redirectUnauthenticated, server.create); 71 | router.patch('/api/server/:serverId', RAS, server.update); 72 | router.delete('/api/server/:serverId', RAS, server.destroy); 73 | 74 | router.get('/api/device', RAS, device.get); 75 | router.post('/api/device/create', RAS, device.create); 76 | router.patch('/api/device/:deviceId', RAS, device.update); 77 | router.delete('/api/device/:deviceId', RAS, device.destroy); 78 | 79 | router.get('/api/subnetwork', RAS, subnetwork.get); 80 | router.post('/api/subnetwork/create', RAS, subnetwork.create); 81 | router.patch('/api/subnetwork/:subnetworkId', RAS, subnetwork.update); 82 | router.delete('/api/subnetwork/:subnetworkId', RAS, subnetwork.destroy); 83 | 84 | router.patch('/api/user/:userId', RAS, user.update); 85 | 86 | app.use(router); 87 | } 88 | 89 | module.exports = routes; -------------------------------------------------------------------------------- /dnms.sql: -------------------------------------------------------------------------------- 1 | -- ---------------------------- 2 | -- Table structure for devices 3 | -- ---------------------------- 4 | DROP TABLE IF EXISTS `devices`; 5 | CREATE TABLE `devices` ( 6 | `id` int unsigned NOT NULL AUTO_INCREMENT, 7 | `serverId` int DEFAULT NULL, 8 | `name` varchar(32) DEFAULT NULL, 9 | `comment` varchar(128) DEFAULT NULL, 10 | `connectTo` varchar(64) DEFAULT '', 11 | `snmpCommunity` varchar(16) DEFAULT 'public', 12 | `connectedTo` int DEFAULT '0', 13 | `os` enum('routeros','airos','other') DEFAULT NULL, 14 | `wireless` tinyint(1) DEFAULT NULL, 15 | `latitude` double DEFAULT NULL, 16 | `longitude` double DEFAULT NULL, 17 | `enabled` tinyint(1) DEFAULT '1', 18 | `visible` tinyint(1) DEFAULT '1', 19 | `createdAt` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 20 | `updatedAt` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 21 | PRIMARY KEY (`id`) 22 | ); 23 | 24 | DROP TABLE IF EXISTS `extras`; 25 | CREATE TABLE `extras` ( 26 | `id` int NOT NULL AUTO_INCREMENT, 27 | `key` varchar(255) DEFAULT NULL, 28 | `value` varchar(255) DEFAULT NULL, 29 | `createdAt` timestamp NULL DEFAULT NULL, 30 | `updatedAt` timestamp NULL DEFAULT NULL, 31 | PRIMARY KEY (`id`) 32 | ); 33 | 34 | -- ---------------------------- 35 | -- Records of extras 36 | -- ---------------------------- 37 | BEGIN; 38 | INSERT INTO `extras` VALUES (1, 'radarTelegramReport', '1', NULL, '2021-05-08 15:31:21'); 39 | COMMIT; 40 | 41 | -- ---------------------------- 42 | -- Table structure for servers 43 | -- ---------------------------- 44 | DROP TABLE IF EXISTS `servers`; 45 | CREATE TABLE `servers` ( 46 | `id` int unsigned NOT NULL AUTO_INCREMENT, 47 | `name` varchar(64) DEFAULT NULL, 48 | `comment` varchar(128) DEFAULT NULL, 49 | `connectTo` varchar(64) DEFAULT NULL, 50 | `apiPort` int DEFAULT '8728', 51 | `username` varchar(16) DEFAULT NULL, 52 | `password` varchar(64) DEFAULT NULL, 53 | `winboxPort` int DEFAULT '8291', 54 | `singleCredential` tinyint(1) DEFAULT '1', 55 | `snmpCommunity` varchar(16) DEFAULT NULL, 56 | `mainInterface` varchar(16) DEFAULT NULL, 57 | `latitude` double DEFAULT NULL, 58 | `longitude` double DEFAULT NULL, 59 | `enabled` tinyint(1) DEFAULT '1', 60 | `createdAt` timestamp NULL DEFAULT NULL, 61 | `updatedAt` timestamp NULL DEFAULT NULL, 62 | PRIMARY KEY (`id`) 63 | ); 64 | 65 | -- ---------------------------- 66 | -- Table structure for sessions 67 | -- ---------------------------- 68 | DROP TABLE IF EXISTS `sessions`; 69 | CREATE TABLE `sessions` ( 70 | `sid` varchar(255) NOT NULL, 71 | `session` text NOT NULL, 72 | `expires` int DEFAULT NULL, 73 | PRIMARY KEY (`sid`) 74 | ); 75 | 76 | -- ---------------------------- 77 | -- Table structure for subnetworks 78 | -- ---------------------------- 79 | DROP TABLE IF EXISTS `subnetworks`; 80 | CREATE TABLE `subnetworks` ( 81 | `id` int NOT NULL AUTO_INCREMENT, 82 | `serverId` int DEFAULT NULL, 83 | `name` varchar(32) DEFAULT NULL, 84 | `deviceId` text, 85 | `enabled` tinyint(1) DEFAULT '1', 86 | `createdAt` timestamp NULL DEFAULT NULL, 87 | `updatedAt` timestamp NULL DEFAULT NULL, 88 | PRIMARY KEY (`id`) 89 | ); 90 | 91 | -- ---------------------------- 92 | -- Table structure for users 93 | -- ---------------------------- 94 | DROP TABLE IF EXISTS `users`; 95 | CREATE TABLE `users` ( 96 | `id` int unsigned NOT NULL AUTO_INCREMENT, 97 | `name` varchar(64) DEFAULT NULL, 98 | `username` varchar(64) DEFAULT NULL, 99 | `password` varchar(255) DEFAULT NULL, 100 | `createdAt` timestamp NULL DEFAULT NULL, 101 | `updatedAt` timestamp NULL DEFAULT NULL, 102 | PRIMARY KEY (`id`) 103 | ); 104 | 105 | -- ---------------------------- 106 | -- Records of users 107 | -- ---------------------------- 108 | BEGIN; 109 | INSERT INTO `users` VALUES (1, 'Daimus Suudi', 'admin', '$2b$10$mtrIX7U61PS1.VaiTbLcbeBt1HwLAJafHom/DViHR3y7nmHymyp3e', NULL, '2021-05-09 00:58:24'); 110 | COMMIT; 111 | -------------------------------------------------------------------------------- /controller/monitor.js: -------------------------------------------------------------------------------- 1 | const { 2 | Server 3 | } = require('../model/server'); 4 | const { 5 | Device 6 | } = require('../model/device'); 7 | const { 8 | Subnetwork 9 | } = require('../model/subnetwork'); 10 | const { 11 | Op 12 | } = require('sequelize'); 13 | 14 | exports.server = async (req, res) => { 15 | const servers = await Server.findAll(); 16 | res.render('monitor/server', { 17 | servers: servers 18 | }); 19 | } 20 | 21 | exports.device = async (req, res) => { 22 | const serverId = req.session.serverId; 23 | const subnetworks = await Subnetwork.findAll({ 24 | where: { 25 | enabled: true, 26 | serverId: serverId 27 | } 28 | }); 29 | let deviceList = []; 30 | if (parseInt(req.query.subnetwork) > 0) { 31 | subnetworks.map(sn => { 32 | if (sn.id == parseInt(req.query.subnetwork)) { 33 | deviceList = JSON.parse(sn.deviceId); 34 | } 35 | }) 36 | } 37 | let filter = { 38 | where: { 39 | serverId: serverId 40 | } 41 | }; 42 | if (deviceList.length > 0) { 43 | filter = { 44 | where: { 45 | id: { 46 | [Op.or]: deviceList 47 | }, 48 | serverId: serverId 49 | } 50 | } 51 | } 52 | const devices = await Device.findAll(filter); 53 | res.render('monitor/device', { 54 | subnetworks: subnetworks, 55 | devices: devices 56 | }); 57 | } 58 | 59 | exports.detail = async (req, res) => { 60 | const snmp = require('../util/snmp'); 61 | const device = await Device.findOne({ 62 | raw: true, 63 | where: { 64 | id: req.params.deviceId 65 | } 66 | }); 67 | let wireless = {}; 68 | if (device.wireless) { 69 | wireless = await snmp.getSections(['ssid', 'mode', 'frequency'], { 70 | id: device.id, 71 | host: device.connectTo, 72 | os: device.os, 73 | community: device.snmpCommunity, 74 | wireless: device.wireless 75 | }).catch(error => { 76 | logger.error(`snmp error ${JSON.stringify(error)}`); 77 | }); 78 | } 79 | res.render('monitor/detail', { 80 | device: device, 81 | wireless: wireless 82 | }); 83 | } 84 | 85 | exports.diagram = async (req, res) => { 86 | const serverId = req.session.serverId; 87 | const subnetworks = await Subnetwork.findAll({ 88 | where: { 89 | enabled: true, 90 | serverId: serverId 91 | } 92 | }); 93 | let deviceList = []; 94 | if (parseInt(req.query.subnetwork) > 0) { 95 | subnetworks.map(sn => { 96 | if (sn.id == parseInt(req.query.subnetwork)) { 97 | deviceList = JSON.parse(sn.deviceId); 98 | } 99 | }) 100 | } 101 | let filter = { 102 | where: { 103 | serverId: serverId 104 | } 105 | }; 106 | if (deviceList.length > 0) { 107 | filter = { 108 | where: { 109 | id: { 110 | [Op.or]: deviceList 111 | }, 112 | serverId: serverId 113 | } 114 | } 115 | } 116 | const devices = await Device.findAll(filter); 117 | res.render('monitor/diagram', { 118 | subnetworks: subnetworks, 119 | devices: devices 120 | }); 121 | } 122 | 123 | exports.map = async (req, res) => { 124 | const serverId = req.session.serverId; 125 | const devices = await Device.findAll({ 126 | where: { 127 | serverId: serverId 128 | } 129 | }); 130 | const server = await Server.findByPk(serverId); 131 | res.render('monitor/map', { 132 | server: server, 133 | devices: devices, 134 | mapbox_token: process.env.MAPBOX_TOKEN 135 | }); 136 | } 137 | 138 | exports.scanner = async (req, res) => { 139 | res.render('monitor/scanner'); 140 | } -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/modules/event-listeners.ts: -------------------------------------------------------------------------------- 1 | import state from './state'; 2 | import { onAction } from './actions'; 3 | import { getNode } from './utils'; 4 | import { SwalOptions } from './options'; 5 | import { CANCEL_KEY } from './options/buttons'; 6 | 7 | import CLASS_NAMES from './class-list'; 8 | const { MODAL, BUTTON, OVERLAY } = CLASS_NAMES; 9 | 10 | const onTabAwayLastButton = (e: KeyboardEvent): void => { 11 | e.preventDefault(); 12 | setFirstButtonFocus(); 13 | }; 14 | 15 | const onTabBackFirstButton = (e: KeyboardEvent): void => { 16 | e.preventDefault(); 17 | setLastButtonFocus(); 18 | }; 19 | 20 | const onKeyUp = (e: KeyboardEvent):void => { 21 | if (!state.isOpen) return; 22 | 23 | switch (e.key) { 24 | case "Escape": return onAction(CANCEL_KEY); 25 | } 26 | }; 27 | 28 | const onKeyDownLastButton = (e: KeyboardEvent): void => { 29 | if (!state.isOpen) return; 30 | 31 | switch (e.key) { 32 | case "Tab": return onTabAwayLastButton(e); 33 | } 34 | }; 35 | 36 | const onKeyDownFirstButton = (e: KeyboardEvent): void => { 37 | if (!state.isOpen) return; 38 | 39 | if (e.key === "Tab" && e.shiftKey) { 40 | return onTabBackFirstButton(e); 41 | } 42 | }; 43 | 44 | 45 | /* 46 | * Set default focus on Confirm-button 47 | */ 48 | const setFirstButtonFocus = (): void => { 49 | const button:HTMLElement = getNode(BUTTON); 50 | 51 | if (button) { 52 | button.tabIndex = 0; 53 | button.focus(); 54 | } 55 | }; 56 | 57 | const setLastButtonFocus = (): void => { 58 | const modal: HTMLElement = getNode(MODAL); 59 | 60 | const buttons: NodeListOf = modal.querySelectorAll(`.${BUTTON}`); 61 | 62 | const lastIndex: number = buttons.length - 1; 63 | const lastButton: any = buttons[lastIndex]; 64 | 65 | if (lastButton) { 66 | lastButton.focus(); 67 | } 68 | }; 69 | 70 | const setTabbingForLastButton = (buttons: NodeListOf): void => { 71 | const lastIndex: number = buttons.length - 1; 72 | const lastButton: Element = buttons[lastIndex]; 73 | 74 | lastButton.addEventListener('keydown', onKeyDownLastButton); 75 | }; 76 | 77 | const setTabbingForFirstButton = (buttons: NodeListOf): void => { 78 | const firstButton: Element = buttons[0]; 79 | 80 | firstButton.addEventListener('keydown', onKeyDownFirstButton); 81 | }; 82 | 83 | const setButtonTabbing = (): void => { 84 | const modal: HTMLElement = getNode(MODAL); 85 | 86 | const buttons: NodeListOf = modal.querySelectorAll(`.${BUTTON}`); 87 | 88 | if (!buttons.length) return; 89 | 90 | setTabbingForLastButton(buttons); 91 | setTabbingForFirstButton(buttons); 92 | }; 93 | 94 | const onOutsideClick = (e: MouseEvent): void => { 95 | const overlay: HTMLElement = getNode(OVERLAY); 96 | 97 | // Don't trigger for children: 98 | if (overlay !== e.target) return; 99 | 100 | return onAction(CANCEL_KEY); 101 | }; 102 | 103 | const setClickOutside = (allow: boolean): void => { 104 | const overlay: HTMLElement = getNode(OVERLAY); 105 | 106 | overlay.removeEventListener('click', onOutsideClick); 107 | 108 | if (allow) { 109 | overlay.addEventListener('click', onOutsideClick); 110 | } 111 | }; 112 | 113 | const setTimer = (ms: number): void => { 114 | if (state.timer) { 115 | clearTimeout(state.timer); 116 | } 117 | 118 | if (ms) { 119 | state.timer = window.setTimeout(() => { 120 | return onAction(CANCEL_KEY); 121 | }, ms); 122 | } 123 | }; 124 | 125 | const addEventListeners = (opts: SwalOptions):void => { 126 | if (opts.closeOnEsc) { 127 | document.addEventListener('keyup', onKeyUp); 128 | } else { 129 | document.removeEventListener('keyup', onKeyUp); 130 | } 131 | 132 | /* So that you don't accidentally confirm something 133 | * dangerous by clicking enter 134 | */ 135 | if (opts.dangerMode) { 136 | setFirstButtonFocus(); 137 | } else { 138 | setLastButtonFocus(); 139 | } 140 | 141 | setButtonTabbing(); 142 | setClickOutside(opts.closeOnClickOutside); 143 | setTimer(opts.timer); 144 | }; 145 | 146 | export default addEventListeners; 147 | -------------------------------------------------------------------------------- /views/monitor/scanner.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Server - DNMS 9 | 10 | <%- include('../partials/head') %> 11 | 12 | 18 | 19 | 20 | 21 |
22 | 23 | <%- include('../partials/header') %> 24 | 25 |
26 |
27 | 28 | 40 | 41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | 54 | <%- include('../partials/footer') %> 55 |
56 |
57 | 58 | 59 | <%- include('../partials/javascript') %> 60 | 61 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /views/report/statistic.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Server - DNMS 9 | 10 | <%- include('../partials/head') %> 11 | 12 | 13 | 14 |
15 | 16 | <%- include('../partials/header') %> 17 | 18 |
19 |
20 | 21 | 38 | 39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | 50 | 51 | <%- include('../partials/footer') %> 52 |
53 |
54 | 55 | 56 | <%- include('../partials/javascript') %> 57 | 58 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/modules/options/buttons.ts: -------------------------------------------------------------------------------- 1 | import { isPlainObject, throwErr } from '../utils'; 2 | 3 | export interface ButtonOptions { 4 | visible?: boolean, 5 | text?: string, 6 | value?: any, 7 | className?: string | Array, 8 | closeModal?: boolean, 9 | }; 10 | 11 | export interface ButtonList { 12 | [buttonNamespace: string]: ButtonOptions | boolean, 13 | }; 14 | 15 | export const CONFIRM_KEY = 'confirm'; 16 | export const CANCEL_KEY = 'cancel'; 17 | 18 | const defaultButton: ButtonOptions = { 19 | visible: true, 20 | text: null, 21 | value: null, 22 | className: '', 23 | closeModal: true, 24 | }; 25 | 26 | const defaultCancelButton: ButtonOptions = Object.assign({}, 27 | defaultButton, { 28 | visible: false, 29 | text: "Cancel", 30 | value: null, 31 | } 32 | ); 33 | 34 | const defaultConfirmButton: ButtonOptions = Object.assign({}, 35 | defaultButton, { 36 | text: "OK", 37 | value: true, 38 | } 39 | ); 40 | 41 | export const defaultButtonList: ButtonList = { 42 | cancel: defaultCancelButton, 43 | confirm: defaultConfirmButton, 44 | }; 45 | 46 | const getDefaultButton = (key: string): ButtonOptions => { 47 | switch (key) { 48 | case CONFIRM_KEY: 49 | return defaultConfirmButton; 50 | 51 | case CANCEL_KEY: 52 | return defaultCancelButton; 53 | 54 | default: 55 | // Capitalize: 56 | const text = key.charAt(0).toUpperCase() + key.slice(1); 57 | 58 | return Object.assign({}, defaultButton, { 59 | text, 60 | value: key, 61 | }); 62 | } 63 | }; 64 | 65 | const normalizeButton = (key: string, param: string | object | boolean): ButtonOptions => { 66 | const button: ButtonOptions = getDefaultButton(key); 67 | 68 | /* 69 | * Use the default button + make it visible 70 | */ 71 | if (param === true) { 72 | return Object.assign({}, button, { 73 | visible: true, 74 | }); 75 | } 76 | 77 | /* Set the text of the button: */ 78 | if (typeof param === "string") { 79 | return Object.assign({}, button, { 80 | visible: true, 81 | text: param, 82 | }); 83 | } 84 | 85 | /* A specified button should always be visible, 86 | * unless "visible" is explicitly set to "false" 87 | */ 88 | if (isPlainObject(param)) { 89 | return Object.assign({ 90 | visible: true, 91 | }, button, param); 92 | } 93 | 94 | return Object.assign({}, button, { 95 | visible: false, 96 | }); 97 | }; 98 | 99 | const normalizeButtonListObj = (obj: any): ButtonList => { 100 | let buttons: ButtonList = {}; 101 | 102 | for (let key of Object.keys(obj)) { 103 | const opts: any = obj[key]; 104 | const button: ButtonOptions = normalizeButton(key, opts); 105 | buttons[key] = button; 106 | } 107 | 108 | /* 109 | * We always need a cancel action, 110 | * even if the button isn't visible 111 | */ 112 | if (!buttons.cancel) { 113 | buttons.cancel = defaultCancelButton; 114 | } 115 | 116 | return buttons; 117 | }; 118 | 119 | const normalizeButtonArray = (arr: any[]): ButtonList => { 120 | let buttonListObj: ButtonList = {}; 121 | 122 | switch (arr.length) { 123 | /* input: ["Accept"] 124 | * result: only set the confirm button text to "Accept" 125 | */ 126 | case 1: 127 | buttonListObj[CANCEL_KEY] = Object.assign({}, defaultCancelButton, { 128 | visible: false, 129 | }); 130 | 131 | break; 132 | 133 | /* input: ["No", "Ok!"] 134 | * result: Set cancel button to "No", and confirm to "Ok!" 135 | */ 136 | case 2: 137 | buttonListObj[CANCEL_KEY] = normalizeButton(CANCEL_KEY, arr[0]); 138 | buttonListObj[CONFIRM_KEY] = normalizeButton(CONFIRM_KEY, arr[1]); 139 | 140 | break; 141 | 142 | default: 143 | throwErr(`Invalid number of 'buttons' in array (${arr.length}). 144 | If you want more than 2 buttons, you need to use an object!`); 145 | } 146 | 147 | return buttonListObj; 148 | }; 149 | 150 | export const getButtonListOpts = (opts: string | object | boolean): ButtonList => { 151 | let buttonListObj: ButtonList = defaultButtonList; 152 | 153 | if (typeof opts === "string") { 154 | buttonListObj[CONFIRM_KEY] = normalizeButton(CONFIRM_KEY, opts); 155 | } else if (Array.isArray(opts)) { 156 | buttonListObj = normalizeButtonArray(opts); 157 | } else if (isPlainObject(opts)) { 158 | buttonListObj = normalizeButtonListObj(opts); 159 | } else if (opts === true) { 160 | buttonListObj = normalizeButtonArray([true, true]); 161 | } else if (opts === false) { 162 | buttonListObj = normalizeButtonArray([false, false]); 163 | } else if (opts === undefined) { 164 | buttonListObj = defaultButtonList; 165 | } 166 | 167 | return buttonListObj; 168 | }; 169 | -------------------------------------------------------------------------------- /views/tool/ping.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Server - DNMS 9 | 10 | <%- include('../partials/head') %> 11 | 12 | 13 | 14 |
15 | 16 | <%- include('../partials/header') %> 17 | 18 |
19 |
20 | 21 | 32 | 33 |
34 |
35 |
36 |
37 |
38 | 39 |
40 | 42 |
43 |
44 |
45 | 46 |
47 | 48 |
49 |
50 |
51 | 52 |
53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
SeqHostTimeSizeTTLStatus
Enter IP or domain, then click PING
71 |
72 |
73 |
74 |
75 | 76 | <%- include('../partials/footer') %> 77 |
78 |
79 | 80 | 81 | <%- include('../partials/javascript') %> 82 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /views/partials/alert-message.ejs: -------------------------------------------------------------------------------- 1 | <% if(locals.alert.length > 0){ %> 2 | <% 3 | var icon = ` 4 | 5 | 6 | 7 | 8 | 9 | `; 10 | var status = locals.alert[0].status; 11 | var title = ''; 12 | switch(locals.alert[0].status){ 13 | case 'success': 14 | icon = ` 15 | 16 | 17 | 18 | `; 19 | title = 'Success!'; 20 | break; 21 | case 'warning': 22 | icon = ` 23 | 24 | 25 | 26 | `; 27 | title = 'Warning!'; 28 | break; 29 | case 'info': 30 | icon = ` 31 | 32 | 33 | 34 | 35 | `; 36 | title = 'Info!'; 37 | break; 38 | case 'error': 39 | case 'fail': 40 | case 'danger': 41 | status = 'danger'; 42 | icon = ` 43 | 44 | 45 | 46 | 47 | `; 48 | title = 'Error!'; 49 | break; 50 | default: 51 | icon = ` 52 | 53 | 54 | 55 | 56 | 57 | `; 58 | break; 59 | } 60 | %> 61 | 62 | 77 | <% } %> -------------------------------------------------------------------------------- /views/monitor/server.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Server - DNMS 9 | 10 | <%- include('../partials/head') %> 11 | 12 | 13 | 14 |
15 | 16 | <%- include('../partials/header') %> 17 | 18 |
19 |
20 | 21 | 34 | 35 |
36 | <% if(locals.servers){ %> 37 | <% servers.forEach((server) => { %> 38 |
39 |
40 |
41 |
42 |
43 |

44 | <%= server.name %> 45 |

46 |
<%= server.comment %>
47 |
48 | Unknown 49 |
50 |
51 |
52 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | <%= server.connectTo %> 63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | <% }) %> 71 | <% } %> 72 |
73 |
74 | 75 | <%- include('../partials/footer') %> 76 |
77 |
78 | 79 | 80 | <%- include('../partials/javascript') %> 81 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /views/tool/arp.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Server - DNMS 9 | 10 | <%- include('../partials/head') %> 11 | 12 | 13 | 14 |
15 | 16 | <%- include('../partials/header') %> 17 | 18 |
19 |
20 | 21 | 40 | 41 |
42 |
43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
AddressMAC AddressInterface
60 |
61 |
62 |
63 |
64 | 65 |
66 | 67 | <%- include('../partials/footer') %> 68 |
69 |
70 | 71 | 72 | <%- include('../partials/javascript') %> 73 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /public/assets/vendor/tabler/static/illustrations/undraw_sign_in_e6hj.svg: -------------------------------------------------------------------------------- 1 | sign_in -------------------------------------------------------------------------------- /views/tool/neighbor.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Server - DNMS 9 | 10 | <%- include('../partials/head') %> 11 | 12 | 13 | 14 |
15 | 16 | <%- include('../partials/header') %> 17 | 18 |
19 |
20 | 21 | 40 | 41 |
42 |
43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
InterfaceAddressMAC Address
60 |
61 |
62 |
63 |
64 | 65 |
66 | 67 | <%- include('../partials/footer') %> 68 |
69 |
70 | 71 | 72 | <%- include('../partials/javascript') %> 73 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /controller/tool.js: -------------------------------------------------------------------------------- 1 | const mikrotikApi = require("../util/mikrotik-api"); 2 | const { 3 | Server 4 | } = require("../model/server"); 5 | const { 6 | Device 7 | } = require('../model/device'); 8 | const { 9 | Op, 10 | where 11 | } = require("sequelize"); 12 | const {logger} = require('../config/logger'); 13 | 14 | exports.ping = (req, res) => { 15 | res.render("tool/ping"); 16 | }; 17 | 18 | exports.log = async (req, res) => { 19 | const serverId = req.session.serverId; 20 | const server = await Server.findOne({ 21 | where: { 22 | id: serverId 23 | } 24 | }); 25 | const conn = { 26 | host: server.connectTo, 27 | user: server.username, 28 | password: server.password, 29 | port: server.apiPort 30 | }; 31 | const connection = mikrotikApi.createConnection(conn); 32 | let result = {}; 33 | await connection.connect().then(async () => { 34 | const logs = await mikrotikApi.write(connection, ["/log/print"]); 35 | connection.close(); 36 | result = { 37 | status: "success", 38 | data: logs, 39 | }; 40 | }).catch((error) => { 41 | logger.error(`mikrotik connection error ${JSON.stringify(error)}`); 42 | result = { 43 | status: "fail", 44 | message: "Error Connection: " + error.toString(), 45 | }; 46 | }); 47 | res.render("tool/log", { 48 | result: result, 49 | }); 50 | }; 51 | 52 | exports.arp = async (req, res) => { 53 | const serverId = req.session.serverId; 54 | const server = await Server.findOne({ 55 | where: { 56 | id: serverId 57 | } 58 | }); 59 | const conn = { 60 | host: server.connectTo, 61 | user: server.username, 62 | password: server.password, 63 | port: server.apiPort 64 | }; 65 | const connection = mikrotikApi.createConnection(conn); 66 | let result = {}; 67 | await connection.connect().then(async () => { 68 | const arps = await mikrotikApi.write(connection, ["/ip/arp/print"]); 69 | connection.close(); 70 | result = { 71 | status: "success", 72 | data: arps, 73 | }; 74 | }).catch((error) => { 75 | logger.error(`mikrotik connection error ${JSON.stringify(error)}`); 76 | result = { 77 | status: "fail", 78 | message: "Error Connection: " + error.toString(), 79 | }; 80 | }); 81 | if (req.query.json) { 82 | return res.jsend.success(result); 83 | } 84 | res.render("tool/arp", { 85 | result: result, 86 | }); 87 | }; 88 | 89 | exports.neighbor = async (req, res) => { 90 | const serverId = req.session.serverId; 91 | const server = await Server.findOne({ 92 | where: { 93 | id: serverId 94 | } 95 | }); 96 | const conn = { 97 | host: server.connectTo, 98 | user: server.username, 99 | password: server.password, 100 | port: server.apiPort 101 | }; 102 | const connection = mikrotikApi.createConnection(conn); 103 | let result = {}; 104 | await connection.connect().then(async () => { 105 | const arps = await mikrotikApi.write(connection, ["/ip/neighbor/print"]); 106 | connection.close(); 107 | result = { 108 | status: "success", 109 | data: arps, 110 | }; 111 | }).catch((error) => { 112 | logger.error(`mikrotik connection error ${JSON.stringify(error)}`); 113 | result = { 114 | status: "fail", 115 | message: "Error Connection: " + error.toString(), 116 | }; 117 | }); 118 | if (req.query.json) { 119 | return res.jsend.success(result); 120 | } 121 | res.render("tool/neighbor", { 122 | result: result, 123 | }); 124 | }; 125 | 126 | exports.lease = async (req, res) => { 127 | const serverId = req.session.serverId; 128 | const server = await Server.findOne({ 129 | where: { 130 | id: serverId 131 | } 132 | }); 133 | const conn = { 134 | host: server.connectTo, 135 | user: server.username, 136 | password: server.password, 137 | port: server.apiPort 138 | }; 139 | const connection = mikrotikApi.createConnection(conn); 140 | let result = {}; 141 | await connection.connect().then(async () => { 142 | const leases = await mikrotikApi.write(connection, ["/ip/dhcp-server/lease/print"]); 143 | connection.close(); 144 | result = { 145 | status: "success", 146 | data: leases, 147 | }; 148 | }).catch((error) => { 149 | logger.error(`mikrotik connection error ${JSON.stringify(error)}`); 150 | result = { 151 | status: "fail", 152 | message: "Error Connection: " + error.toString(), 153 | }; 154 | }); 155 | 156 | if (req.query.json) { 157 | return res.jsend.success(result); 158 | } 159 | res.render("tool/lease", { 160 | result: result, 161 | }); 162 | }; 163 | 164 | exports.host = async (req, res) => { 165 | const serverId = req.session.serverId; 166 | const server = await Server.findOne({ 167 | where: { 168 | id: serverId 169 | } 170 | }); 171 | const conn = { 172 | host: server.connectTo, 173 | user: server.username, 174 | password: server.password, 175 | port: server.apiPort 176 | }; 177 | const connection = mikrotikApi.createConnection(conn); 178 | let result = {}; 179 | await connection.connect().then(async () => { 180 | const leases = await mikrotikApi.write(connection, ["/ip/hotspot/host/print"]); 181 | connection.close(); 182 | result = { 183 | status: "success", 184 | data: leases, 185 | }; 186 | }).catch((error) => { 187 | logger.error(`mikrotik connection error ${JSON.stringify(error)}`); 188 | result = { 189 | status: "fail", 190 | message: "Error Connection: " + error.toString(), 191 | }; 192 | }); 193 | 194 | if (req.query.json) { 195 | return res.jsend.success(result); 196 | } 197 | res.render("tool/host", { 198 | result: result, 199 | }); 200 | }; -------------------------------------------------------------------------------- /views/tool/host.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Server - DNMS 9 | 10 | <%- include('../partials/head') %> 11 | 12 | 13 | 14 |
15 | 16 | <%- include('../partials/header') %> 17 | 18 |
19 |
20 | 21 | 40 | 41 |
42 |
43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
NameAddressMAC AddressServer
61 |
62 |
63 |
64 |
65 | 66 |
67 | 68 | <%- include('../partials/footer') %> 69 |
70 |
71 | 72 | 73 | <%- include('../partials/javascript') %> 74 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /public/assets/vendor/sweetalert/modules/options/index.ts: -------------------------------------------------------------------------------- 1 | import { SwalParams } from '../../core'; 2 | 3 | import { 4 | throwErr, 5 | isPlainObject, 6 | ordinalSuffixOf, 7 | } from '../utils'; 8 | 9 | import { 10 | ButtonList, 11 | getButtonListOpts, 12 | defaultButtonList 13 | } from './buttons'; 14 | 15 | import { 16 | getContentOpts, 17 | ContentOptions, 18 | } from './content'; 19 | 20 | import { 21 | DEPRECATED_OPTS, 22 | logDeprecation, 23 | } from './deprecations'; 24 | 25 | 26 | /* 27 | * The final object that we transform the given params into 28 | */ 29 | export interface SwalOptions { 30 | title: string, 31 | text: string, 32 | icon: string, 33 | buttons: ButtonList | Array, 34 | content: ContentOptions, 35 | className: string, 36 | closeOnClickOutside: boolean, 37 | closeOnEsc: boolean, 38 | dangerMode: boolean, 39 | timer: number, 40 | }; 41 | 42 | const defaultOpts: SwalOptions = { 43 | title: null, 44 | text: null, 45 | icon: null, 46 | buttons: defaultButtonList, 47 | content: null, 48 | className: null, 49 | closeOnClickOutside: true, 50 | closeOnEsc: true, 51 | dangerMode: false, 52 | timer: null, 53 | }; 54 | 55 | /* 56 | * Default options customizeable through "setDefaults": 57 | */ 58 | let userDefaults: SwalOptions = Object.assign({}, defaultOpts); 59 | 60 | export const setDefaults = (opts: object): void => { 61 | userDefaults = Object.assign({}, defaultOpts, opts); 62 | }; 63 | 64 | 65 | /* 66 | * Since the user can set both "button" and "buttons", 67 | * we need to make sure we pick one of the options 68 | */ 69 | const pickButtonParam = (opts: any): object => { 70 | const singleButton: string|object = opts && opts.button; 71 | const buttonList: object = opts && opts.buttons; 72 | 73 | if (singleButton !== undefined && buttonList !== undefined) { 74 | throwErr(`Cannot set both 'button' and 'buttons' options!`); 75 | } 76 | 77 | if (singleButton !== undefined) { 78 | return { 79 | confirm: singleButton, 80 | }; 81 | } else { 82 | return buttonList; 83 | } 84 | }; 85 | 86 | // Example 0 -> 1st 87 | const indexToOrdinal = (index: number): string => ordinalSuffixOf(index + 1); 88 | 89 | const invalidParam = (param: any, index: number): void => { 90 | throwErr(`${indexToOrdinal(index)} argument ('${param}') is invalid`); 91 | }; 92 | 93 | const expectOptionsOrNothingAfter = (index: number, allParams: SwalParams): void => { 94 | let nextIndex = (index + 1); 95 | let nextParam = allParams[nextIndex]; 96 | 97 | if (!isPlainObject(nextParam) && nextParam !== undefined) { 98 | throwErr(`Expected ${indexToOrdinal(nextIndex)} argument ('${nextParam}') to be a plain object`); 99 | } 100 | }; 101 | 102 | const expectNothingAfter = (index: number, allParams: SwalParams): void => { 103 | let nextIndex = (index + 1); 104 | let nextParam = allParams[nextIndex]; 105 | 106 | if (nextParam !== undefined) { 107 | throwErr(`Unexpected ${indexToOrdinal(nextIndex)} argument (${nextParam})`); 108 | } 109 | }; 110 | 111 | /* 112 | * Based on the number of arguments, their position and their type, 113 | * we return an object that's merged into the final SwalOptions 114 | */ 115 | const paramToOption = (opts: any, param: any, index: number, allParams: SwalParams): object => { 116 | 117 | const paramType = (typeof param); 118 | const isString = (paramType === "string"); 119 | const isDOMNode = (param instanceof Element); 120 | 121 | if (isString) { 122 | if (index === 0) { 123 | // Example: swal("Hi there!"); 124 | return { 125 | text: param, 126 | }; 127 | } 128 | 129 | else if (index === 1) { 130 | // Example: swal("Wait!", "Are you sure you want to do this?"); 131 | // (The text is now the second argument) 132 | return { 133 | text: param, 134 | title: allParams[0], 135 | }; 136 | } 137 | 138 | else if (index === 2) { 139 | // Example: swal("Wait!", "Are you sure?", "warning"); 140 | expectOptionsOrNothingAfter(index, allParams); 141 | 142 | return { 143 | icon: param, 144 | }; 145 | } 146 | 147 | else { 148 | invalidParam(param, index); 149 | } 150 | } 151 | 152 | else if (isDOMNode && index === 0) { 153 | // Example: swal(); 154 | expectOptionsOrNothingAfter(index, allParams); 155 | 156 | return { 157 | content: param, 158 | }; 159 | } 160 | 161 | else if (isPlainObject(param)) { 162 | expectNothingAfter(index, allParams); 163 | 164 | return param; 165 | } 166 | 167 | else { 168 | invalidParam(param, index); 169 | } 170 | 171 | }; 172 | 173 | /* 174 | * No matter if the user calls swal with 175 | * - swal("Oops!", "An error occured!", "error") or 176 | * - swal({ title: "Oops!", text: "An error occured!", icon: "error" }) 177 | * ... we always want to transform the params into the second version 178 | */ 179 | export const getOpts = (...params: SwalParams): SwalOptions => { 180 | let opts = {}; 181 | 182 | params.forEach((param, index) => { 183 | let changes: object = paramToOption(opts, param, index, params); 184 | Object.assign(opts, changes); 185 | }); 186 | 187 | // Since Object.assign doesn't deep clone, 188 | // we need to do this: 189 | let buttonListOpts = pickButtonParam(opts); 190 | opts.buttons = getButtonListOpts(buttonListOpts); 191 | delete opts.button; 192 | 193 | opts.content = getContentOpts(opts.content); 194 | 195 | const finalOptions: SwalOptions = Object.assign({}, defaultOpts, userDefaults, opts); 196 | 197 | // Check if the users uses any deprecated options: 198 | Object.keys(finalOptions).forEach(optionName => { 199 | if (DEPRECATED_OPTS[optionName]) { 200 | logDeprecation(optionName); 201 | } 202 | }); 203 | 204 | return finalOptions; 205 | }; 206 | -------------------------------------------------------------------------------- /views/tool/lease.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Server - DNMS 9 | 10 | <%- include('../partials/head') %> 11 | 12 | 13 | 14 |
15 | 16 | <%- include('../partials/header') %> 17 | 18 |
19 |
20 | 21 | 40 | 41 |
42 |
43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
NameAddressMAC AddressServerStatus
62 |
63 |
64 |
65 |
66 | 67 |
68 | 69 | <%- include('../partials/footer') %> 70 |
71 |
72 | 73 | 74 | <%- include('../partials/javascript') %> 75 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /util/snmp.js: -------------------------------------------------------------------------------- 1 | const netSnmp = require('net-snmp'); 2 | const {logger} = require('../config/logger'); 3 | 4 | const oids = { 5 | routeros: [{ 6 | name: 'ssid', 7 | oid: '1.3.6.1.4.1.14988.1.1.1.3.1.4' 8 | }, 9 | { 10 | name: 'frequency', 11 | oid: '1.3.6.1.4.1.14988.1.1.1.3.1.7', 12 | }, 13 | { 14 | name: 'radioName', 15 | oid: '1.3.6.1.4.1.14988.1.1.1.2.1.20', 16 | }, 17 | { 18 | name: 'signalStrength', 19 | oid: '1.3.6.1.4.1.14988.1.1.1.2.1.3', 20 | }, 21 | { 22 | name: 'overallCcq', 23 | oid: '1.3.6.1.4.1.14988.1.1.1.3.1.10', 24 | }, 25 | { 26 | name: 'txRate', 27 | oid: '1.3.6.1.4.1.14988.1.1.1.2.1.8', 28 | }, 29 | { 30 | name: 'rxRate', 31 | oid: '1.3.6.1.4.1.14988.1.1.1.2.1.9', 32 | }, 33 | { 34 | name: 'uptime', 35 | oid: '1.3.6.1.4.1.14988.1.1.1.2.1.11', 36 | }, 37 | { 38 | name: 'clientCount', 39 | oid: '1.3.6.1.4.1.14988.1.1.1.3.1.6', 40 | }, 41 | { 42 | name: 'noiseFloor', 43 | oid: '1.3.6.1.4.1.14988.1.1.1.3.1.9', 44 | }, 45 | { 46 | name: 'mode', 47 | fn: 'findOutMode' 48 | } 49 | ], 50 | airos: [{ 51 | name: 'ssid', 52 | oid: '1.3.6.1.4.1.41112.1.4.5.1.2' 53 | }, 54 | { 55 | name: 'frequency', 56 | oid: '1.3.6.1.4.1.41112.1.4.1.1.4' 57 | }, 58 | { 59 | name: 'radioName', 60 | oid: '1.3.6.1.4.1.41112.1.4.7.1.2', 61 | }, 62 | { 63 | name: 'signalStrength', 64 | oid: '1.3.6.1.4.1.41112.1.4.5.1.5', 65 | }, 66 | { 67 | name: 'overallCcq', 68 | oid: '1.3.6.1.4.1.41112.1.4.5.1.7', 69 | }, 70 | { 71 | name: 'txRate', 72 | oid: '1.3.6.1.4.1.41112.1.4.5.1.9', 73 | }, 74 | { 75 | name: 'rxRate', 76 | oid: '1.3.6.1.4.1.41112.1.4.5.1.10', 77 | }, 78 | { 79 | name: 'uptime', 80 | oid: '1.3.6.1.4.1.41112.1.4.7.1.15', 81 | }, 82 | { 83 | name: 'clientCount', 84 | oid: '1.3.6.1.4.1.41112.1.4.5.1.15', 85 | }, 86 | { 87 | name: 'noiseFloor', 88 | oid: '1.3.6.1.4.1.41112.1.4.5.1.8', 89 | }, 90 | { 91 | name: 'mode', 92 | fn: 'findOutMode' 93 | } 94 | ] 95 | }; 96 | 97 | module.exports.getSections = async (sections, device, options = {}) => { 98 | let result = {}; 99 | if (device.os !== 'routeros' && device.os !== 'airos'){ 100 | return result; 101 | } 102 | for (const oid of oids[device.os]) { 103 | if (sections.indexOf(oid.name) != -1) { 104 | if (oid.oid !== undefined) { 105 | let value = Array(); 106 | let snmpResult = await getByOid(oid.oid, device, options); 107 | snmpResult.forEach(element => { 108 | value.push(element.value); 109 | }); 110 | result[oid.name] = value; 111 | } else { 112 | result[oid.name] = await global[oid.fn](result, device); 113 | } 114 | } 115 | } 116 | return result; 117 | } 118 | 119 | global.findOutMode = async (tempResult, device) => { 120 | if (device.os == 'airos') { 121 | const oid = '1.3.6.1.4.1.41112.1.4.1.1.2'; 122 | let wirelessMode = 'unknown'; 123 | let snmpResult = await getByOid(oid, device); 124 | if (snmpResult) { 125 | switch (snmpResult[0].value) { 126 | case '1': 127 | wirelessMode = 'station'; 128 | break; 129 | case '2': 130 | wirelessMode = 'ap'; 131 | break; 132 | case '3': 133 | wirelessMode = 'aprepeater'; 134 | break; 135 | case '4': 136 | wirelessMode = 'apwds'; 137 | break; 138 | default: 139 | wirelessMode = 'unknown'; 140 | break; 141 | } 142 | } 143 | return wirelessMode; 144 | } 145 | 146 | let totalClientCount = 0; 147 | let totalSignalStrength = 0; 148 | let wirelessMode = 'unknown'; 149 | 150 | if (tempResult.signalStrength !== undefined) { 151 | totalSignalStrength += parseInt(tempResult.signalStrength.length); 152 | } else { 153 | for (const oid of oids.routeros) { 154 | if (oid.name == 'signalStrength') { 155 | let snmpResult = await getByOid(oid.oid, device); 156 | if (snmpResult) { 157 | totalSignalStrength += parseInt(snmpResult.length); 158 | } 159 | } 160 | } 161 | } 162 | 163 | if (totalSignalStrength == 1) { 164 | if (tempResult.clientCount !== undefined) { 165 | tempResult.clientCount.map(clientCount => { 166 | totalClientCount += parseInt(clientCount); 167 | }); 168 | } else { 169 | for (const oid of oids.routeros) { 170 | if (oid.name == 'clientCount') { 171 | let snmpResult = await getByOid(oid.oid, device); 172 | if (snmpResult) { 173 | snmpResult.map(value => { 174 | totalClientCount += parseInt(value.value); 175 | }); 176 | } 177 | } 178 | } 179 | } 180 | } 181 | 182 | if (totalSignalStrength > 1) { 183 | wirelessMode = 'ap'; 184 | } else if (totalSignalStrength == 1 && totalClientCount == 1) { 185 | wirelessMode = 'ap'; 186 | } else if (totalSignalStrength == 1 && totalClientCount == 0) { 187 | wirelessMode = 'station'; 188 | } 189 | 190 | return wirelessMode; 191 | } 192 | 193 | global.getUbntMode = (device) => { 194 | 195 | } 196 | 197 | const getByOid = (oid, device, options = {}) => { 198 | const session = netSnmp.createSession(device.host, device.snmpCommunity, options); 199 | 200 | return new Promise((resolve, reject) => { 201 | let results = new Array(); 202 | 203 | function doneCb(error) { 204 | if (error) { 205 | logger.error(`snmp error ${JSON.stringify(error)}`); 206 | reject(error); 207 | } else { 208 | resolve(results); 209 | } 210 | } 211 | 212 | function feedCb(varbinds) { 213 | for (var i = 0; i < varbinds.length; i++) { 214 | if (netSnmp.isVarbindError(varbinds[i])) { 215 | logger.error(`snmp error ${JSON.stringify(netSnmp.varbindError(varbinds[i]))}`); 216 | reject(netSnmp.varbindError(varbinds[i])); 217 | } else { 218 | let result = varbinds[i]; 219 | result.value = varbinds[i].value.toString(); 220 | results.push(result); 221 | } 222 | } 223 | } 224 | 225 | var maxRepetitions = 20; 226 | session.subtree(oid, maxRepetitions, feedCb, doneCb); 227 | }); 228 | } -------------------------------------------------------------------------------- /views/tool/log.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Server - DNMS 9 | 10 | <%- include('../partials/head') %> 11 | 12 | 13 | 14 |
15 | 16 | <%- include('../partials/header') %> 17 | 18 |
19 |
20 | 21 | 40 | 41 |
42 |
43 |
44 |
45 |
46 | 47 |
48 | 62 |
63 |
64 | 65 |
66 |
67 |
68 |
69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 |
TimeTopicsMessage
84 |
85 |
86 |
87 |
88 | 89 |
90 | 91 | <%- include('../partials/footer') %> 92 |
93 |
94 | 95 | 96 | <%- include('../partials/javascript') %> 97 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /views/setting/user.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | User - DNMS 9 | 10 | <%- include('../partials/head') %> 11 | 12 | 13 | 14 |
15 | 16 | <%- include('../partials/header') %> 17 | 18 |
19 |
20 | 21 | 32 | 33 |
34 |
35 |
36 |
38 |
39 | 40 |
41 | 42 | 45 | 46 | 47 | 48 | 49 | 51 |
52 |
53 |
54 | 55 |
56 | 57 | 60 | 61 | 62 | 63 | 64 | 66 |
67 |
68 |

Leave it blank if you won't to change password

69 |
70 | 71 |
72 | 73 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 85 |
86 |
87 |
88 | 89 |
90 | 91 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 103 |
104 |
105 | 106 |
107 |
108 |
109 |
110 |
111 | 112 | <%- include('../partials/footer') %> 113 |
114 |
115 | 116 | 117 | <%- include('../partials/javascript') %> 118 | 119 | 120 | -------------------------------------------------------------------------------- /views/scheduler/radar.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Offline Radar Configuration - DNMS 9 | 10 | <%- include('../partials/head') %> 11 | 12 | 13 | 14 |
15 | 16 | <%- include('../partials/header') %> 17 | 18 |
19 |
20 | 21 | 32 | 33 |
34 |
35 |
36 | 40 |
41 |
42 | 46 |
47 |
48 | 49 |
50 |
51 | 53 |
54 |
55 | 65 |
66 |
67 |
68 |
69 | <% if (locals.devices){ %> 70 | <% locals.devices.forEach(device => { %> 71 |
72 | 88 |
89 | <% }) %> 90 | <% } %> 91 |
92 |
93 | 108 |
109 |
110 |
111 |
112 |
113 | 114 |
115 | 116 | <%- include('../partials/footer') %> 117 |
118 |
119 | 120 | 121 | <%- include('../partials/javascript') %> 122 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /views/monitor/device.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Server - DNMS 9 | 10 | <%- include('../partials/head') %> 11 | 17 | 18 | 19 | 20 |
21 | 22 | <%- include('../partials/header') %> 23 | 24 |
25 |
26 | 27 | 39 | 40 |
41 |
42 |
43 |
44 |
45 | 54 |
55 |
56 | <% if(locals.devices){ %> 57 | <% locals.devices.forEach((device) => { %> 58 |
59 |
60 |
61 |
62 |
63 |

64 | <%= device.name %> 66 |

67 |
<%= device.comment %>
68 |
69 | Unknown 70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | <% }) %> 81 | <% } %> 82 |
83 |
84 |
85 |
86 |
87 |
88 | 89 | <%- include('../partials/footer') %> 90 |
91 |
92 | 93 | 94 | <%- include('../partials/javascript') %> 95 | 155 | 156 | 157 | --------------------------------------------------------------------------------