├── .gitattributes ├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── app.js ├── config └── default.json ├── docs ├── config.png ├── dashboard.png ├── hub_details_1.png ├── hub_details_2.png ├── hublist.png └── server.png ├── modules ├── cache.js └── softEther.js ├── package-lock.json ├── package.json ├── public ├── app │ ├── app.js │ ├── common │ │ ├── autoDataTable.template.html │ │ ├── common.js │ │ ├── infoCard.template.html │ │ ├── keyValueTable.template.html │ │ └── loadingIndicator.template.html │ ├── config │ │ ├── config.js │ │ └── config.template.html │ ├── dashboard │ │ ├── dashboard.js │ │ └── dashboard.template.html │ ├── hubs │ │ ├── hubs.hubdetails.template.html │ │ ├── hubs.hublist.template.html │ │ ├── hubs.js │ │ └── hubs.template.html │ ├── server │ │ ├── server.connections.template.html │ │ ├── server.js │ │ └── server.template.html │ └── utils.js ├── css │ ├── animate.min.css │ ├── bootstrap.min.css │ ├── light-bootstrap-dashboard.css │ └── pe-icon-7-stroke.css ├── fonts │ ├── Pe-icon-7-stroke.eot │ ├── Pe-icon-7-stroke.svg │ ├── Pe-icon-7-stroke.ttf │ └── Pe-icon-7-stroke.woff ├── img │ └── favicon.ico ├── index.html └── js │ ├── bootstrap-notify.js │ ├── bootstrap-select.js │ ├── bootstrap.min.js │ ├── chartist.min.js │ ├── jquery.3.2.1.min.js │ └── light-bootstrap-dashboard.js ├── routes ├── hub.js ├── server.js ├── static.js └── ui.js └── scripts ├── vpncmd_hubinfofull.txt ├── vpncmd_hubsessioninfo.txt └── vpncmd_serverinfo.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | public/css/* linguist-vendored 2 | bootstrap.min.css linguist-vendored=false 3 | public/js/* linguist-vendored 4 | *.min.js linguist-vendored=false 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # parcel-bundler cache (https://parceljs.org/) 61 | .cache 62 | 63 | # next.js build output 64 | .next 65 | 66 | # nuxt.js build output 67 | .nuxt 68 | 69 | # vuepress build output 70 | .vuepress/dist 71 | 72 | # Serverless directories 73 | .serverless 74 | 75 | ?temp 76 | 77 | config/local.json -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceFolder}\\app.js" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SoftEtherAdmin: A Web UI for SoftEther VPN Server 2 | SoftEtherAdmin is a web based UI for the SoftEther VPN server. Currently it only supports read operations, and the feature set is not complete! 3 | The UI design is the [Light Bootstarp Dashboard theme](https://github.com/creativetimofficial/light-bootstrap-dashboard) by the lovely folks at [Creative Tim](https://www.creative-tim.com/). 4 | 5 | 6 | # Install 7 | ## Prerequisites 8 | * SoftEther vpncmd (https://www.softether.org/) 9 | * node.js v8+ (https://nodejs.org/en/) 10 | * [optional] pm2 (http://pm2.keymetrics.io/) 11 | * [optional] a web server (ex.: nginx) 12 | 13 | 14 | ## Linux 15 | ### Getting the files 16 | First, you need to clone/download the files: 17 | ```shell 18 | # GIT clone (Note: git should be installed on your system!) 19 | cd /srv 20 | sudo git clone https://github.com/notisrac/SoftEtherAdmin.git 21 | 22 | ## OR ## 23 | 24 | # Download (Note: unzip should be installed on your system!) 25 | wget -O SoftEtherAdmin.zip https://github.com/notisrac/SoftEtherAdmin/archive/master.zip 26 | sudo unzip SoftEtherAdmin.zip -d /srv/SoftEtherAdmin 27 | ``` 28 | 29 | ### Restore npm packages 30 | Before running the application, you must restore the npm packages! 31 | ```shell 32 | cd /srv/SoftEtherAdmin 33 | sudo npm install 34 | ``` 35 | 36 | ### Configure the application 37 | Follow the instructions in the [config section](#config) to set up the application. 38 | You should have a config something like this: 39 | ```json 40 | { 41 | "serverPort": 8000, 42 | "softEther" : { 43 | "address" : "localhost", 44 | "port" : 5555, 45 | "vpncmdPath" : "/usr/local/vpnserver/vpncmd", 46 | "password" : "supersecretpassword1" 47 | } 48 | } 49 | ``` 50 | 51 | ### Test 52 | At this stage the application should be runnable: 53 | ```shell 54 | node app.js 55 | ``` 56 | Open another shell, and: 57 | ```shell 58 | wget http://localhost:8000/ 59 | ``` 60 | 61 | 62 | ### Managing 63 | The recommended way of managing node.js apps is to use `pm2`: 64 | ```shell 65 | # first, you need to install pm2 globally 66 | npm install pm2 -g 67 | # enter the dir wher SoftEtherAdmin is installed 68 | cd /srv/SoftEtherAdmin 69 | # Register the app with pm2 70 | pm2 start app.js --name "softetheradmin" 71 | ``` 72 | 73 | More info in the [pm2 section](#pm2) 74 | 75 | 76 | ### Web server 77 | For serving the app through a web server, all you need to do is, configure the web server as a reverse proxy pointing to the application's port. 78 | **nginx example**: 79 | ``` 80 | server { 81 | listen 80; 82 | listen [::]:80; 83 | server_name SoftEtherAdmin; 84 | 85 | location / { 86 | proxy_pass http://localhost:8000; # <- this is where out app is listening 87 | proxy_http_version 1.1; 88 | proxy_set_header Upgrade $http_upgrade; 89 | proxy_set_header Connection 'upgrade'; 90 | proxy_set_header Host $host; 91 | proxy_cache_bypass $http_upgrade; 92 | } 93 | } 94 | ``` 95 | 96 | 97 | ## Windows 98 | Download the file https://github.com/notisrac/SoftEtherAdmin/archive/master.zip 99 | Then extract it to a folder. We will be using: 100 | ``` 101 | C:\NodeApps\ 102 | ``` 103 | 104 | ### Restore npm packages 105 | ```shell 106 | cd C:\NodeApps\SoftEtherAdmin 107 | npm install 108 | ``` 109 | 110 | ### Configure the application 111 | Follow the instructions in the [config section](#config) to set up the application. 112 | You should have a config something like this: 113 | ```json 114 | { 115 | "serverPort": 8000, 116 | "softEther" : { 117 | "address" : "localhost", 118 | "port" : 5555, 119 | "vpncmdPath" : "C:\\Program Files\\SoftEther\\vpncmd.exe", 120 | "password" : "supersecretpassword1" 121 | } 122 | } 123 | ``` 124 | 125 | ### Test 126 | At this stage the application should be runnable: 127 | ```shell 128 | node app.js 129 | ``` 130 | Open a browser, and navigate to: `http://localhost:8000/` 131 | 132 | ### Managing 133 | The recommended way of managing node.js apps is to use `pm2`: 134 | 135 | ```shell 136 | # first, you need to install pm2 globally 137 | npm install pm2 -g 138 | ``` 139 | 140 | Before you can use pm2 on windows, there are a few things that needs to be done: 141 | 142 | **pm2 folder** 143 | - Create a folder for pm2: `C:\NodeApps\_pm2` 144 | 145 | **PM2_HOME environment variable** 146 | - Create a new system level environment variable `PM2_HOME`, with the value `C:\NodeApps\_pm2` 147 | - (You are better off restarting windows, for changes to take effect) 148 | - Ensure that the variable is set up correctly `echo %PM2_HOME%` 149 | 150 | **Register the app in pm2** 151 | ```shell 152 | # enter the dir wher SoftEtherAdmin is installed 153 | cd /srv/SoftEtherAdmin 154 | # Register the app with pm2 155 | pm2 start app.js --name "softetheradmin" 156 | # If everything went fine, save the config 157 | pm2 save 158 | ``` 159 | 160 | **Create a service out of pm2** 161 | 162 | We will do this with the help of [pm2-windows-service](https://www.npmjs.com/package/pm2-windows-service) 163 | ```shell 164 | ## Make sure, you do this in an ADMINISTRATOR cmd ## 165 | # install 166 | npm install -g pm2-windows-service 167 | # Create the service 168 | pm2-service-install -n PM2 169 | ``` 170 | Answer the setup questions like this: 171 | - ? Perform environment setup (recommended)? **Yes** 172 | - ? Set PM2_HOME? **Yes** 173 | - ? PM2_HOME value (this path should be accessible to the service user and 174 | should not contain any "user-context" variables [e.g. %APPDATA%]): **C:\NodeApps\_pm2** 175 | - ? Set PM2_SERVICE_SCRIPTS (the list of start-up scripts for pm2)? **No** 176 | - ? Set PM2_SERVICE_PM2_DIR (the location of the global pm2 to use with the service)? [recommended] **Yes** 177 | - ? Specify the directory containing the pm2 version to be used by the 178 | service C:\USERS\NOTI\APPDATA\ROAMING\NPM\node_modules\pm2\index.js **press enter** 179 | - PM2 service installed and started. 180 | 181 | 182 | Big thanks to Walter Accantelli for the Windows instructions: 183 | https://blog.cloudboost.io/nodejs-pm2-startup-on-windows-db0906328d75 184 | 185 | More info in the [pm2 section](#pm2) 186 | 187 | 188 | ## Config 189 | The configuration of the app is handled by the `config` node module (https://www.npmjs.com/package/config). 190 | By default you need to modify the `config/default.json` file: 191 | ```json 192 | { 193 | "serverPort": 8000, 194 | "softEther" : { 195 | "address" : "localhost", 196 | "port" : 5555, 197 | "vpncmdPath" : "/usr/local/vpnserver/vpncmd", 198 | "password" : "supersecretpassword1" 199 | } 200 | } 201 | ``` 202 | Where: 203 | - **serverPort**: The port, where the node.js server should be listening. _Note: if you install multiple instances of the app, each needs a different port!_ 204 | - **address**: Address of the VPN server. If you want to host this app on the same server as the VPN server, use `localhost` 205 | - **port**: The port of the VPN server 206 | - **vpncmdPath**: Absolute path to the vpncmd application. (On Windows, use double backslashes: `c:\\...\\...`!) 207 | - **password**: Server admin level password 208 | 209 | _Note: if you have cloned the repo, it is advisable to have the config in a `config/local.json` file. This way, when pulling new versions of the repo, your config is not overwritten!_ 210 | 211 | More config file related info can be found here: https://github.com/lorenwest/node-config/wiki/Configuration-Files 212 | 213 | ## pm2 214 | pm2 is a process manager for node.js. It can monitor your app, launch it on server startup, etc. 215 | 216 | Install: 217 | ```shell 218 | npm install pm2 -g 219 | ``` 220 | Register the app with pm2 221 | ```shell 222 | pm2 start app.js --name "softetheradmin" 223 | ``` 224 | Check the current status of the app 225 | ```shell 226 | pm2 show softetheradmin 227 | ``` 228 | List all apps manged by pm2 229 | ```shell 230 | pm2 list 231 | ``` 232 | You can also stop `pm2 stop softetheradmin` and restart `pm2 restart softetheradmin` the app. 233 | 234 | # Troubleshooting 235 | ## Run the app from the console 236 | ```shell 237 | cd /srv/softetheradmin 238 | node app.js 239 | ``` 240 | This should result in a `Server listening on port: ` message, where `` is the value of the `serverPort` config setting. 241 | If there were error while starting the app or running it, it will be printed out here. 242 | 243 | ## How the app is accessing VPN server data 244 | It uses the `vpncmd` application, that is distributed with the SoftEther VPN Server installer. Here are two examples: 245 | 246 | This one is run on a linux box, and retrieves the list of hubs: 247 | ```shell 248 | /usr/local/vpnserver/vpncmd : /SERVER /PASSWORD: /CSV /CMD HubList 249 | ``` 250 | 251 | This one is run on a windows machine, and executes all the commands in the `scripts/vpncmd_hubinfofull.txt` file on the selected hub: 252 | ```shell 253 | "c:\Program Files\SoftEther VPN Client Manager\vpncmd_x64.exe" : /SERVER /PASSWORD: /CSV /ADMINHUB: /IN:"scripts/vpncmd_hubinfofull.txt" 254 | ``` 255 | 256 | ## pm2 monitoring 257 | ```shell 258 | pm2 monit 259 | ``` 260 | 261 | # TODO 262 | - [ ] Add logging (use [winston](https://www.npmjs.com/package/winston)) 263 | - [ ] The second (third, etc.) api calls for the same resource should wait for the first one to finish 264 | - [ ] Display more information about the server and the hubs (ex.: ports, openVPN status, etc.) 265 | - [ ] Make server logs downloadable 266 | - [ ] Add functionality to modify settings of the server 267 | - [ ] Extract the softEther.js file into a standalone module once it is done 268 | - [ ] Make it a bit more colorful by adding SoftEther's icons (?) 269 | - [ ] User management??? (not quite sure, this is needed) 270 | 271 | # How to contribute 272 | Pull requests are always welcome! :) 273 | 274 | # Screenshots 275 | ![Dashboard][dashboard_png] 276 | ![Server][server_png] 277 | ![Hub list][hublist_png] 278 | ![Hub details 1][hub_details_1_png] 279 | ![Hub details 2][hub_details_2_png] 280 | ![Config][config_png] 281 | 282 | [![Analytics](https://ga-beacon.appspot.com/UA-122950438-1/SoftEtherAdmin)](https://github.com/igrigorik/ga-beacon) 283 | 284 | 285 | [config_png]: docs/config.png "Config" 286 | [dashboard_png]: docs/dashboard.png "Dashboard" 287 | [hub_details_1_png]: docs/hub_details_1.png "Hub details 1" 288 | [hub_details_2_png]: docs/hub_details_2.png "Hub details 2" 289 | [hublist_png]: docs/hublist.png "Hub list" 290 | [server_png]: docs/server.png "Server" 291 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | var bodyParser = require('body-parser'); 4 | global.config = require('config'); 5 | var path = require('path'); 6 | var cors = require('cors'); 7 | 8 | var route_api_server = require('./routes/server'); 9 | var route_api_hub = require('./routes/hub'); 10 | var route_ui = require('./routes/ui'); 11 | var route_static = require('./routes/static'); 12 | 13 | // parse application/x-www-form-urlencoded 14 | app.use(bodyParser.urlencoded({'extended':'true'})); 15 | // parse application/json 16 | app.use(bodyParser.json()); 17 | // parse application/vnd.api+json as json 18 | app.use(bodyParser.json({ type: 'application/vnd.api+json' })); 19 | 20 | // set up the static files 21 | app.use(route_static); 22 | // set up the API endpoint 23 | app.use('/api/server', route_api_server); 24 | app.use('/api/hub', route_api_hub); 25 | app.use('/api', function(req, res, next) { 26 | res.sendStatus(404); 27 | }); 28 | // everything else should go to the ui 29 | app.use('/*', route_ui); 30 | 31 | 32 | // start the server 33 | var serverPort = global.config.get('serverPort'); 34 | app.listen(serverPort); 35 | console.log('Server listening on port: ' + serverPort); 36 | 37 | 38 | // TODO add logging (ex.: winston) https://www.npmjs.com/package/winston 39 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "serverPort": 8000, 3 | "softEther" : { 4 | "address" : "localhost", 5 | "port" : 5555, 6 | "vpncmdPath" : "path to the vpncmd executable", 7 | "password" : "" 8 | } 9 | } -------------------------------------------------------------------------------- /docs/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notisrac/SoftEtherAdmin/a088a5824e7eaaa110db7f1e5625c0f6b14e05c7/docs/config.png -------------------------------------------------------------------------------- /docs/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notisrac/SoftEtherAdmin/a088a5824e7eaaa110db7f1e5625c0f6b14e05c7/docs/dashboard.png -------------------------------------------------------------------------------- /docs/hub_details_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notisrac/SoftEtherAdmin/a088a5824e7eaaa110db7f1e5625c0f6b14e05c7/docs/hub_details_1.png -------------------------------------------------------------------------------- /docs/hub_details_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notisrac/SoftEtherAdmin/a088a5824e7eaaa110db7f1e5625c0f6b14e05c7/docs/hub_details_2.png -------------------------------------------------------------------------------- /docs/hublist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notisrac/SoftEtherAdmin/a088a5824e7eaaa110db7f1e5625c0f6b14e05c7/docs/hublist.png -------------------------------------------------------------------------------- /docs/server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notisrac/SoftEtherAdmin/a088a5824e7eaaa110db7f1e5625c0f6b14e05c7/docs/server.png -------------------------------------------------------------------------------- /modules/cache.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Memory cache middleware for express 3 | */ 4 | 5 | var memCache = require('memory-cache'); 6 | 7 | // original: https://glitch.com/edit/#!/server-side-cache-express 8 | 9 | var cache = function (duration) { 10 | return function (req, res, next) { 11 | let key = '__express__' + req.originalUrl || req.url; 12 | let cachedContent = memCache.get(key); 13 | if (cachedContent) { 14 | res.send(cachedContent); 15 | return; 16 | } 17 | else { 18 | res.sendResponse = res.send; 19 | res.send = function (body) { 20 | memCache.put(key, body, duration * 1000); 21 | res.sendResponse(body); 22 | } 23 | next(); 24 | } 25 | } 26 | } 27 | 28 | module.exports = cache; -------------------------------------------------------------------------------- /modules/softEther.js: -------------------------------------------------------------------------------- 1 | var exec = require('child-process-promise').exec; 2 | const parse = require('csv-parse'); 3 | const parseSync = require('csv-parse/lib/sync'); 4 | // the defult options: parse table headers 5 | const csvParseOptions = { 6 | columns: true, 7 | delimiter: ',', 8 | cast: true 9 | }; 10 | // key-value pair type: don't parse table headers, start from row 2 11 | const csvParseOptions_flat = { 12 | columns: null, 13 | delimiter: ',', 14 | cast: true, 15 | from: 2 16 | }; 17 | const csvParseOptions_object = { 18 | columns: true, 19 | delimiter: ',', 20 | cast: true, 21 | objname: 'Item' 22 | }; 23 | 24 | // https://www.softether.org/4-docs/1-manual/6._Command_Line_Management_Utility_Manual 25 | 26 | 27 | var softEther = { 28 | /* 29 | * SERVER COMMANDS 30 | */ 31 | about: function () { 32 | return this.executeHeaderlessCommand('About'); 33 | }, 34 | serverInfoGet: function () { 35 | return this.executeCSVCommand('ServerInfoGet', null, null, 0, 0, csvParseOptions_flat, true); 36 | }, 37 | serverStatusGet: function () { 38 | return this.executeCSVCommand('ServerStatusGet', null, null, 0, 0, csvParseOptions_flat, true); 39 | }, 40 | listenerList: function () { 41 | return this.executeCSVCommand('ListenerList'); 42 | }, 43 | clusterSettingGet: function () { 44 | return this.executeCSVCommand('ClusterSettingGet'); 45 | }, 46 | syslogGet: function () { 47 | return this.executeCSVCommand('SyslogGet'); 48 | }, 49 | connectionList: function () { 50 | return this.executeCSVCommand('ConnectionList'); 51 | }, 52 | connectionGet: function (id) { 53 | return this.executeCSVCommand('ConnectionList', id); 54 | }, 55 | bridgeDeviceList: function () { 56 | return this.executeCSVCommand('BridgeDeviceList'); 57 | }, 58 | bridgeList: function () { 59 | return this.executeCSVCommand('BridgeList'); 60 | }, 61 | caps: function () { 62 | return this.executeCSVCommand('Caps', null, null, 0, 0, csvParseOptions_flat, true); 63 | }, 64 | configGet: function () { 65 | return this.executeHeaderlessCommand('ConfigGet', null, null, 2, 0); 66 | }, 67 | routerList: function () { 68 | return this.executeCSVCommand('RouterList'); 69 | }, 70 | routerIfList: function (name) { 71 | return this.executeCSVCommand('RouterIfList', name); 72 | }, 73 | routerTableList: function (name) { 74 | return this.executeCSVCommand('RouterTableList', name); 75 | }, 76 | logFileList: function () { 77 | return this.executeCSVCommand('LogFileList'); 78 | }, 79 | hubList: function () { 80 | return this.executeCSVCommand('HubList'); 81 | }, 82 | hub: function (name) { 83 | return this.executeCSVCommand('Hub', name); 84 | }, 85 | check: function () { 86 | return this.executeHeaderlessCommand('Check'); 87 | }, 88 | etherIpClientList: function () { 89 | return this.executeCSVCommand('EtherIpClientList'); 90 | }, 91 | openVpnGet: function () { 92 | return this.executeCSVCommand('OpenVpnGet'); 93 | }, 94 | sstpGet: function () { 95 | return this.executeCSVCommand('SstpGet'); 96 | }, 97 | vpnOverIcmpDnsGet: function () { 98 | return this.executeCSVCommand('VpnOverIcmpDnsGet'); 99 | }, 100 | dynamicDnsGetStatus: function () { 101 | return this.executeCSVCommand('DynamicDnsGetStatus'); 102 | }, 103 | vpnAzureGetStatus: function () { 104 | return this.executeCSVCommand('VpnAzureGetStatus'); 105 | }, 106 | 107 | /* 108 | * HUB COMMANDS 109 | */ 110 | hubStatus: function (name) { 111 | return this.executeCSVCommand('StatusGet', null, name, 0, 0, csvParseOptions_flat, true); 112 | }, 113 | hubUserList: function (name) { 114 | return this.executeCSVCommand('UserList', null, name); 115 | }, 116 | 117 | /* 118 | * COMMON 119 | */ 120 | executeFile: function (fileName, hub = null, settings = {}) { 121 | /* 122 | settings format 123 | { 124 | StatusGet : { 125 | csv: true, 126 | flatten: true, 127 | trimStart: 0, 128 | trimEnd: 0 129 | } 130 | } 131 | */ 132 | var self = this; 133 | return new Promise(function (resolve, reject) { 134 | self.executeCommand(null, null, hub, true, fileName, 1, 0) 135 | .then(function (result) { 136 | var retValue = {}; 137 | var re = new RegExp('.*[\/\w]?>(.*)', 'gm'); 138 | var matches = []; 139 | var m; 140 | while (m = re.exec(result)) { 141 | matches.push(m); 142 | } 143 | for (let i = 0; i < matches.length; i++) { 144 | // get the actual command 145 | const fullCommand = matches[i][0]; 146 | const command = matches[i][1]; 147 | // get the settings for this command 148 | var cmdSettings = settings[command]; 149 | // get the data after the command 150 | var cmdLocation = matches[i].index + fullCommand.length + 1; 151 | var nextCmdLocation = result.length; 152 | if (matches[i + 1]) { 153 | nextCmdLocation = result.indexOf(matches[i + 1][0]) - 1; 154 | } 155 | var data = result.substring(cmdLocation, nextCmdLocation); 156 | // format the data according to the settings 157 | if (cmdSettings) { 158 | if (cmdSettings.trimStart) { 159 | data = self.trimStart(data, cmdSettings.trimStart); 160 | } 161 | if (cmdSettings.trimEnd) { 162 | data = self.trimEnd(data, cmdSettings.trimEnd); 163 | } 164 | if (cmdSettings.csv) { 165 | var parseOptions = csvParseOptions; 166 | if (cmdSettings.flatten) { 167 | parseOptions = csvParseOptions_flat; 168 | } 169 | data = parseSync(data, parseOptions); 170 | if (cmdSettings.flatten) { 171 | data = self.flattenData(data); 172 | } 173 | } 174 | } 175 | // add the data to the collection with the command name as the name 176 | retValue[command] = data; 177 | } 178 | resolve(retValue); 179 | }) 180 | .catch(function (err) { 181 | reject(err); 182 | }); 183 | }); 184 | }, 185 | 186 | /* 187 | * COMMAND FUNCTIONS 188 | */ 189 | 190 | executeHeaderlessCommand: function (command, commandParams = null, hub = null, trimStartLines = 0, trimEndLines = 0) { 191 | // execute the command, with csv enabled 192 | return this.executeCommand(command, commandParams, hub, true, null, trimStartLines, trimEndLines); 193 | }, 194 | 195 | executeCSVCommand: function (command, commandParams = null, hub = null, trimStartLines = 0, trimEndLines = 0, parseOptions = csvParseOptions, flatten = false) { 196 | var self = this; 197 | 198 | return new Promise(function (resolve, reject) { 199 | // execute the command, with csv enabled 200 | self.executeCommand(command, commandParams, hub, true, null, trimStartLines, trimEndLines) 201 | .then(function (result) { 202 | // try to parse the resulting csv 203 | parse(result, parseOptions, function (err, output) { 204 | if (err) { 205 | reject(err); 206 | } 207 | resolve((flatten) ? self.flattenData(output) : output); 208 | }); 209 | }) 210 | .catch(function (err) { 211 | reject(err); 212 | }); 213 | }); 214 | }, 215 | 216 | // ./vpncmd localhost:port /SERVER /PASSWORD:asd /HUB:VPN /CSV /CMD hublist 217 | executeCommand: function (softEtherCommand, softEtherCommandParams, hubName, enableCSV, fileName, trimStartLines = 0, trimEndLines = 0) { 218 | var self = this; 219 | var command = this.assembleCommand(softEtherCommand, softEtherCommandParams, hubName, enableCSV, fileName); 220 | // TODO removeme 221 | //console.log(command); 222 | 223 | return new Promise(function (resolve, reject) { 224 | exec(command) 225 | .then(function (result) { 226 | var stdout = result.stdout; 227 | var stderr = result.stderr; 228 | console.log(stdout); 229 | console.log(stderr); 230 | if (stderr || stdout.startsWith('Error occurred.')) { 231 | reject('Error while executing command "' + softEtherCommand + '": \r\n' + stderr + '\r\n' + stdout); 232 | } 233 | 234 | if (trimStartLines > 0) { 235 | stdout = self.trimStart(stdout, trimStartLines); 236 | } 237 | if (trimEndLines > 0) { 238 | stdout = self.trimEnd(stdout, trimEndLines); 239 | } 240 | 241 | resolve(stdout); 242 | }) 243 | .catch(function (err) { 244 | console.log(err); 245 | reject('Error while executing command "' + softEtherCommand + '": \r\n' + err); 246 | }); 247 | }); 248 | }, 249 | 250 | assembleCommand: function (softEtherCommand, softEtherCommandParams, hubName, enableCSV, fileName) { 251 | var command = ''; 252 | // vpncmd executeable 253 | command += '"' + global.config.get('softEther.vpncmdPath') + '"'; 254 | // address:port 255 | command += ' ' + global.config.get('softEther.address'); 256 | var port = global.config.get('softEther.port'); 257 | if (port) { 258 | command += ':' + port + ''; 259 | } 260 | // we want to administer the server 261 | command += ' /SERVER'; 262 | // password 263 | var pwd = global.config.get('softEther.password'); 264 | if (pwd) { 265 | command += ' /PASSWORD:' + pwd; 266 | } 267 | // should the return value be in csv 268 | if (enableCSV) { 269 | command += ' /CSV'; 270 | } 271 | // select the hub 272 | if (hubName) { 273 | //command += ' /HUB:' + hubName; // this would require the hub password, and we only have the server pwd 274 | command += ' /ADMINHUB:' + hubName; 275 | } 276 | // if the filename is specified 277 | if (fileName) { 278 | command += ' /IN:"' + fileName + '"'; 279 | } 280 | else { 281 | // the command to execute on the server 282 | command += ' /CMD ' + softEtherCommand; 283 | if (softEtherCommandParams) { 284 | command += ' ' + softEtherCommandParams; 285 | } 286 | } 287 | 288 | return command; 289 | }, 290 | 291 | /* 292 | * HELPER FUNCTIONS 293 | */ 294 | 295 | flattenData: function (data) { 296 | var retData = {}; 297 | data.forEach(element => { 298 | // console.log(element); 299 | retData[element[0]] = element[1]; 300 | }); 301 | return retData; 302 | }, 303 | 304 | trimStart: function (data, count) { 305 | if (count > 0) { 306 | for (let i = 0; i < count; i++) { 307 | data = data.substring(data.indexOf("\n") + 1); 308 | } 309 | } 310 | return data; 311 | }, 312 | 313 | trimEnd: function (data, count) { 314 | if (count > 0) { 315 | for (let i = 0; i < count; i++) { 316 | data = data.substring(data.lastIndexOf("\n") + 1, -1); 317 | } 318 | } 319 | 320 | return data; 321 | } 322 | }; 323 | 324 | 325 | module.exports = softEther; -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "softetheradmin", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "body-parser": { 8 | "version": "1.18.3", 9 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", 10 | "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", 11 | "requires": { 12 | "bytes": "3.0.0", 13 | "content-type": "1.0.4", 14 | "debug": "2.6.9", 15 | "depd": "1.1.2", 16 | "http-errors": "1.6.3", 17 | "iconv-lite": "0.4.23", 18 | "on-finished": "2.3.0", 19 | "qs": "6.5.2", 20 | "raw-body": "2.3.3", 21 | "type-is": "1.6.16" 22 | }, 23 | "dependencies": { 24 | "bytes": { 25 | "version": "3.0.0", 26 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 27 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 28 | }, 29 | "content-type": { 30 | "version": "1.0.4", 31 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 32 | "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=" 33 | }, 34 | "debug": { 35 | "version": "2.6.9", 36 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 37 | "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", 38 | "requires": { 39 | "ms": "2.0.0" 40 | }, 41 | "dependencies": { 42 | "ms": { 43 | "version": "2.0.0", 44 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 45 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 46 | } 47 | } 48 | }, 49 | "depd": { 50 | "version": "1.1.2", 51 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 52 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 53 | }, 54 | "http-errors": { 55 | "version": "1.6.3", 56 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", 57 | "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", 58 | "requires": { 59 | "depd": "1.1.2", 60 | "inherits": "2.0.3", 61 | "setprototypeof": "1.1.0", 62 | "statuses": "1.5.0" 63 | }, 64 | "dependencies": { 65 | "inherits": { 66 | "version": "2.0.3", 67 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 68 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 69 | }, 70 | "setprototypeof": { 71 | "version": "1.1.0", 72 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 73 | "integrity": "sha1-0L2FU2iHtv58DYGMuWLZ2RxU5lY=" 74 | }, 75 | "statuses": { 76 | "version": "1.5.0", 77 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 78 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 79 | } 80 | } 81 | }, 82 | "iconv-lite": { 83 | "version": "0.4.23", 84 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", 85 | "integrity": "sha1-KXhx9jvlB63Pv8pxXQzQ7thOmmM=", 86 | "requires": { 87 | "safer-buffer": "2.1.2" 88 | }, 89 | "dependencies": { 90 | "safer-buffer": { 91 | "version": "2.1.2", 92 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 93 | "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo=" 94 | } 95 | } 96 | }, 97 | "on-finished": { 98 | "version": "2.3.0", 99 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 100 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 101 | "requires": { 102 | "ee-first": "1.1.1" 103 | }, 104 | "dependencies": { 105 | "ee-first": { 106 | "version": "1.1.1", 107 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 108 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 109 | } 110 | } 111 | }, 112 | "qs": { 113 | "version": "6.5.2", 114 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 115 | "integrity": "sha1-yzroBuh0BERYTvFUzo7pjUA/PjY=" 116 | }, 117 | "raw-body": { 118 | "version": "2.3.3", 119 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", 120 | "integrity": "sha1-GzJOzmtXBuFThVvBFIxlu39uoMM=", 121 | "requires": { 122 | "bytes": "3.0.0", 123 | "http-errors": "1.6.3", 124 | "iconv-lite": "0.4.23", 125 | "unpipe": "1.0.0" 126 | }, 127 | "dependencies": { 128 | "unpipe": { 129 | "version": "1.0.0", 130 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 131 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 132 | } 133 | } 134 | }, 135 | "type-is": { 136 | "version": "1.6.16", 137 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", 138 | "integrity": "sha1-+JzjQVQcZysl7nrjxz3uOyvlAZQ=", 139 | "requires": { 140 | "media-typer": "0.3.0", 141 | "mime-types": "2.1.19" 142 | }, 143 | "dependencies": { 144 | "media-typer": { 145 | "version": "0.3.0", 146 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 147 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 148 | }, 149 | "mime-types": { 150 | "version": "2.1.19", 151 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", 152 | "integrity": "sha1-ceRkU3p++BwV8tudl+kT/A/2BvA=", 153 | "requires": { 154 | "mime-db": "1.35.0" 155 | }, 156 | "dependencies": { 157 | "mime-db": { 158 | "version": "1.35.0", 159 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", 160 | "integrity": "sha1-BWnWV0ZkkSg3CWY603mpm5DZq0c=" 161 | } 162 | } 163 | } 164 | } 165 | } 166 | } 167 | }, 168 | "child-process-promise": { 169 | "version": "2.2.1", 170 | "resolved": "https://registry.npmjs.org/child-process-promise/-/child-process-promise-2.2.1.tgz", 171 | "integrity": "sha1-RzChHvYQ+tRQuPIjx50x172tgHQ=", 172 | "requires": { 173 | "cross-spawn": "4.0.2", 174 | "node-version": "1.2.0", 175 | "promise-polyfill": "6.1.0" 176 | }, 177 | "dependencies": { 178 | "cross-spawn": { 179 | "version": "4.0.2", 180 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", 181 | "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", 182 | "requires": { 183 | "lru-cache": "4.1.3", 184 | "which": "1.3.1" 185 | }, 186 | "dependencies": { 187 | "lru-cache": { 188 | "version": "4.1.3", 189 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", 190 | "integrity": "sha1-oRdc80lt/IQ2wVbDNLSVWZK85pw=", 191 | "requires": { 192 | "pseudomap": "1.0.2", 193 | "yallist": "2.1.2" 194 | }, 195 | "dependencies": { 196 | "pseudomap": { 197 | "version": "1.0.2", 198 | "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", 199 | "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" 200 | }, 201 | "yallist": { 202 | "version": "2.1.2", 203 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", 204 | "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" 205 | } 206 | } 207 | }, 208 | "which": { 209 | "version": "1.3.1", 210 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 211 | "integrity": "sha1-pFBD1U9YBTFtqNYvn1CRjT2nCwo=", 212 | "requires": { 213 | "isexe": "2.0.0" 214 | }, 215 | "dependencies": { 216 | "isexe": { 217 | "version": "2.0.0", 218 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 219 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" 220 | } 221 | } 222 | } 223 | } 224 | }, 225 | "node-version": { 226 | "version": "1.2.0", 227 | "resolved": "https://registry.npmjs.org/node-version/-/node-version-1.2.0.tgz", 228 | "integrity": "sha1-NP3j/6jhFJvTI5g0ed2mIOG1Bg0=" 229 | }, 230 | "promise-polyfill": { 231 | "version": "6.1.0", 232 | "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-6.1.0.tgz", 233 | "integrity": "sha1-36lpQ+qcEh/KTem1hoyznTRy4Fc=" 234 | } 235 | } 236 | }, 237 | "config": { 238 | "version": "2.0.1", 239 | "resolved": "https://registry.npmjs.org/config/-/config-2.0.1.tgz", 240 | "integrity": "sha1-mVzMgXVGBXjWRqwKLkAY/6RMoEY=", 241 | "requires": { 242 | "json5": "1.0.1" 243 | }, 244 | "dependencies": { 245 | "json5": { 246 | "version": "1.0.1", 247 | "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", 248 | "integrity": "sha1-d5+wAYYE+oVOrL9iUhgNg1Q+Pb4=", 249 | "requires": { 250 | "minimist": "1.2.0" 251 | }, 252 | "dependencies": { 253 | "minimist": { 254 | "version": "1.2.0", 255 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 256 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" 257 | } 258 | } 259 | } 260 | } 261 | }, 262 | "cors": { 263 | "version": "2.8.4", 264 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.4.tgz", 265 | "integrity": "sha1-K9OB8usgECAQXNUOpZ2mMJBpRoY=", 266 | "requires": { 267 | "object-assign": "4.1.1", 268 | "vary": "1.1.2" 269 | } 270 | }, 271 | "csv-parse": { 272 | "version": "2.5.0", 273 | "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-2.5.0.tgz", 274 | "integrity": "sha1-ZXSJl+zDcZxZRiLbG56g4ut9Vrs=" 275 | }, 276 | "express": { 277 | "version": "4.16.3", 278 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", 279 | "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", 280 | "requires": { 281 | "accepts": "1.3.5", 282 | "array-flatten": "1.1.1", 283 | "body-parser": "1.18.2", 284 | "content-disposition": "0.5.2", 285 | "content-type": "1.0.4", 286 | "cookie": "0.3.1", 287 | "cookie-signature": "1.0.6", 288 | "debug": "2.6.9", 289 | "depd": "1.1.2", 290 | "encodeurl": "1.0.2", 291 | "escape-html": "1.0.3", 292 | "etag": "1.8.1", 293 | "finalhandler": "1.1.1", 294 | "fresh": "0.5.2", 295 | "merge-descriptors": "1.0.1", 296 | "methods": "1.1.2", 297 | "on-finished": "2.3.0", 298 | "parseurl": "1.3.2", 299 | "path-to-regexp": "0.1.7", 300 | "proxy-addr": "2.0.4", 301 | "qs": "6.5.1", 302 | "range-parser": "1.2.0", 303 | "safe-buffer": "5.1.1", 304 | "send": "0.16.2", 305 | "serve-static": "1.13.2", 306 | "setprototypeof": "1.1.0", 307 | "statuses": "1.4.0", 308 | "type-is": "1.6.16", 309 | "utils-merge": "1.0.1", 310 | "vary": "1.1.2" 311 | }, 312 | "dependencies": { 313 | "accepts": { 314 | "version": "1.3.5", 315 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", 316 | "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", 317 | "requires": { 318 | "mime-types": "2.1.19", 319 | "negotiator": "0.6.1" 320 | }, 321 | "dependencies": { 322 | "mime-types": { 323 | "version": "2.1.19", 324 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", 325 | "integrity": "sha1-ceRkU3p++BwV8tudl+kT/A/2BvA=", 326 | "requires": { 327 | "mime-db": "1.35.0" 328 | }, 329 | "dependencies": { 330 | "mime-db": { 331 | "version": "1.35.0", 332 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", 333 | "integrity": "sha1-BWnWV0ZkkSg3CWY603mpm5DZq0c=" 334 | } 335 | } 336 | }, 337 | "negotiator": { 338 | "version": "0.6.1", 339 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 340 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 341 | } 342 | } 343 | }, 344 | "array-flatten": { 345 | "version": "1.1.1", 346 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 347 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 348 | }, 349 | "body-parser": { 350 | "version": "1.18.2", 351 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", 352 | "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", 353 | "requires": { 354 | "bytes": "3.0.0", 355 | "content-type": "1.0.4", 356 | "debug": "2.6.9", 357 | "depd": "1.1.2", 358 | "http-errors": "1.6.3", 359 | "iconv-lite": "0.4.19", 360 | "on-finished": "2.3.0", 361 | "qs": "6.5.1", 362 | "raw-body": "2.3.2", 363 | "type-is": "1.6.16" 364 | }, 365 | "dependencies": { 366 | "bytes": { 367 | "version": "3.0.0", 368 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 369 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 370 | }, 371 | "http-errors": { 372 | "version": "1.6.3", 373 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", 374 | "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", 375 | "requires": { 376 | "depd": "1.1.2", 377 | "inherits": "2.0.3", 378 | "setprototypeof": "1.1.0", 379 | "statuses": "1.4.0" 380 | }, 381 | "dependencies": { 382 | "inherits": { 383 | "version": "2.0.3", 384 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 385 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 386 | } 387 | } 388 | }, 389 | "iconv-lite": { 390 | "version": "0.4.19", 391 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", 392 | "integrity": "sha1-90aPYBNfXl2tM5nAqBvpoWA6CCs=" 393 | }, 394 | "raw-body": { 395 | "version": "2.3.2", 396 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", 397 | "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", 398 | "requires": { 399 | "bytes": "3.0.0", 400 | "http-errors": "1.6.2", 401 | "iconv-lite": "0.4.19", 402 | "unpipe": "1.0.0" 403 | }, 404 | "dependencies": { 405 | "http-errors": { 406 | "version": "1.6.2", 407 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", 408 | "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", 409 | "requires": { 410 | "depd": "1.1.1", 411 | "inherits": "2.0.3", 412 | "setprototypeof": "1.0.3", 413 | "statuses": "1.4.0" 414 | }, 415 | "dependencies": { 416 | "depd": { 417 | "version": "1.1.1", 418 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", 419 | "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" 420 | }, 421 | "inherits": { 422 | "version": "2.0.3", 423 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 424 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 425 | }, 426 | "setprototypeof": { 427 | "version": "1.0.3", 428 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", 429 | "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" 430 | } 431 | } 432 | }, 433 | "unpipe": { 434 | "version": "1.0.0", 435 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 436 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 437 | } 438 | } 439 | } 440 | } 441 | }, 442 | "content-disposition": { 443 | "version": "0.5.2", 444 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 445 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 446 | }, 447 | "content-type": { 448 | "version": "1.0.4", 449 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 450 | "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=" 451 | }, 452 | "cookie": { 453 | "version": "0.3.1", 454 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 455 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 456 | }, 457 | "cookie-signature": { 458 | "version": "1.0.6", 459 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 460 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 461 | }, 462 | "debug": { 463 | "version": "2.6.9", 464 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 465 | "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", 466 | "requires": { 467 | "ms": "2.0.0" 468 | }, 469 | "dependencies": { 470 | "ms": { 471 | "version": "2.0.0", 472 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 473 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 474 | } 475 | } 476 | }, 477 | "depd": { 478 | "version": "1.1.2", 479 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 480 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 481 | }, 482 | "encodeurl": { 483 | "version": "1.0.2", 484 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 485 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 486 | }, 487 | "escape-html": { 488 | "version": "1.0.3", 489 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 490 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 491 | }, 492 | "etag": { 493 | "version": "1.8.1", 494 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 495 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 496 | }, 497 | "finalhandler": { 498 | "version": "1.1.1", 499 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", 500 | "integrity": "sha1-7r9O2EAHnIP0JJA4ydcDAIMBsQU=", 501 | "requires": { 502 | "debug": "2.6.9", 503 | "encodeurl": "1.0.2", 504 | "escape-html": "1.0.3", 505 | "on-finished": "2.3.0", 506 | "parseurl": "1.3.2", 507 | "statuses": "1.4.0", 508 | "unpipe": "1.0.0" 509 | }, 510 | "dependencies": { 511 | "unpipe": { 512 | "version": "1.0.0", 513 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 514 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 515 | } 516 | } 517 | }, 518 | "fresh": { 519 | "version": "0.5.2", 520 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 521 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 522 | }, 523 | "merge-descriptors": { 524 | "version": "1.0.1", 525 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 526 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 527 | }, 528 | "methods": { 529 | "version": "1.1.2", 530 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 531 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 532 | }, 533 | "on-finished": { 534 | "version": "2.3.0", 535 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 536 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 537 | "requires": { 538 | "ee-first": "1.1.1" 539 | }, 540 | "dependencies": { 541 | "ee-first": { 542 | "version": "1.1.1", 543 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 544 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 545 | } 546 | } 547 | }, 548 | "parseurl": { 549 | "version": "1.3.2", 550 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 551 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 552 | }, 553 | "path-to-regexp": { 554 | "version": "0.1.7", 555 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 556 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 557 | }, 558 | "proxy-addr": { 559 | "version": "2.0.4", 560 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", 561 | "integrity": "sha1-7PxzO/Iv+Mb0B/onUye5q2fki5M=", 562 | "requires": { 563 | "forwarded": "0.1.2", 564 | "ipaddr.js": "1.8.0" 565 | }, 566 | "dependencies": { 567 | "forwarded": { 568 | "version": "0.1.2", 569 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 570 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 571 | }, 572 | "ipaddr.js": { 573 | "version": "1.8.0", 574 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", 575 | "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" 576 | } 577 | } 578 | }, 579 | "qs": { 580 | "version": "6.5.1", 581 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", 582 | "integrity": "sha1-NJzfbu+J7EXBLX1es/wMhwNDptg=" 583 | }, 584 | "range-parser": { 585 | "version": "1.2.0", 586 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 587 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 588 | }, 589 | "safe-buffer": { 590 | "version": "5.1.1", 591 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 592 | "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=" 593 | }, 594 | "send": { 595 | "version": "0.16.2", 596 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", 597 | "integrity": "sha1-bsyh4PjBVtFBWXVZhI32RzCmu8E=", 598 | "requires": { 599 | "debug": "2.6.9", 600 | "depd": "1.1.2", 601 | "destroy": "1.0.4", 602 | "encodeurl": "1.0.2", 603 | "escape-html": "1.0.3", 604 | "etag": "1.8.1", 605 | "fresh": "0.5.2", 606 | "http-errors": "1.6.3", 607 | "mime": "1.4.1", 608 | "ms": "2.0.0", 609 | "on-finished": "2.3.0", 610 | "range-parser": "1.2.0", 611 | "statuses": "1.4.0" 612 | }, 613 | "dependencies": { 614 | "destroy": { 615 | "version": "1.0.4", 616 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 617 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 618 | }, 619 | "http-errors": { 620 | "version": "1.6.3", 621 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", 622 | "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", 623 | "requires": { 624 | "depd": "1.1.2", 625 | "inherits": "2.0.3", 626 | "setprototypeof": "1.1.0", 627 | "statuses": "1.4.0" 628 | }, 629 | "dependencies": { 630 | "inherits": { 631 | "version": "2.0.3", 632 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 633 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 634 | } 635 | } 636 | }, 637 | "mime": { 638 | "version": "1.4.1", 639 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", 640 | "integrity": "sha1-Eh+evEnjdm8xGnbh+hyAA8SwOqY=" 641 | }, 642 | "ms": { 643 | "version": "2.0.0", 644 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 645 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 646 | } 647 | } 648 | }, 649 | "serve-static": { 650 | "version": "1.13.2", 651 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", 652 | "integrity": "sha1-CV6Ecv1bRiN9tQzkhqQ/S4bGzsE=", 653 | "requires": { 654 | "encodeurl": "1.0.2", 655 | "escape-html": "1.0.3", 656 | "parseurl": "1.3.2", 657 | "send": "0.16.2" 658 | } 659 | }, 660 | "setprototypeof": { 661 | "version": "1.1.0", 662 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 663 | "integrity": "sha1-0L2FU2iHtv58DYGMuWLZ2RxU5lY=" 664 | }, 665 | "statuses": { 666 | "version": "1.4.0", 667 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 668 | "integrity": "sha1-u3PURtonlhBu/MG2AaJT1sRr0Ic=" 669 | }, 670 | "type-is": { 671 | "version": "1.6.16", 672 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", 673 | "integrity": "sha1-+JzjQVQcZysl7nrjxz3uOyvlAZQ=", 674 | "requires": { 675 | "media-typer": "0.3.0", 676 | "mime-types": "2.1.19" 677 | }, 678 | "dependencies": { 679 | "media-typer": { 680 | "version": "0.3.0", 681 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 682 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 683 | }, 684 | "mime-types": { 685 | "version": "2.1.19", 686 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", 687 | "integrity": "sha1-ceRkU3p++BwV8tudl+kT/A/2BvA=", 688 | "requires": { 689 | "mime-db": "1.35.0" 690 | }, 691 | "dependencies": { 692 | "mime-db": { 693 | "version": "1.35.0", 694 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", 695 | "integrity": "sha1-BWnWV0ZkkSg3CWY603mpm5DZq0c=" 696 | } 697 | } 698 | } 699 | } 700 | }, 701 | "utils-merge": { 702 | "version": "1.0.1", 703 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 704 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 705 | }, 706 | "vary": { 707 | "version": "1.1.2", 708 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 709 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 710 | } 711 | } 712 | }, 713 | "memory-cache": { 714 | "version": "0.2.0", 715 | "resolved": "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz", 716 | "integrity": "sha1-eJCwHVLADI68nVM+H46xfjA0hxo=" 717 | }, 718 | "object-assign": { 719 | "version": "4.1.1", 720 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 721 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 722 | }, 723 | "path": { 724 | "version": "0.12.7", 725 | "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", 726 | "integrity": "sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=", 727 | "requires": { 728 | "process": "0.11.10", 729 | "util": "0.10.4" 730 | }, 731 | "dependencies": { 732 | "process": { 733 | "version": "0.11.10", 734 | "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", 735 | "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" 736 | }, 737 | "util": { 738 | "version": "0.10.4", 739 | "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", 740 | "integrity": "sha1-OqASW/5mikZy3liFfTrOJ+y3aQE=", 741 | "requires": { 742 | "inherits": "2.0.3" 743 | }, 744 | "dependencies": { 745 | "inherits": { 746 | "version": "2.0.3", 747 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 748 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 749 | } 750 | } 751 | } 752 | } 753 | }, 754 | "vary": { 755 | "version": "1.1.2", 756 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 757 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 758 | } 759 | } 760 | } 761 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "softetheradmin", 3 | "version": "1.0.0", 4 | "description": "Web UI for administering a SoftEther server", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "softether", 11 | "webui", 12 | "ui", 13 | "admin", 14 | "administration" 15 | ], 16 | "author": "noti", 17 | "license": "GPL-3.0", 18 | "dependencies": { 19 | "body-parser": "^1.18.3", 20 | "child-process-promise": "^2.2.1", 21 | "config": "^2.0.1", 22 | "cors": "^2.8.4", 23 | "csv-parse": "^2.5.0", 24 | "express": "^4.16.3", 25 | "memory-cache": "^0.2.0", 26 | "path": "^0.12.7" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /public/app/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var softEtherAdminApp = angular.module('softEtherAdminApp', [ 4 | 'ngRoute', 5 | 'commonModule', 6 | 'dashboardModule', 7 | 'serverModule', 8 | 'configModule', 9 | 'hubsModule' 10 | ]); 11 | 12 | softEtherAdminApp.config(['$locationProvider', '$routeProvider', 13 | function config($locationProvider, $routeProvider) { 14 | $locationProvider.html5Mode(true); 15 | $locationProvider.hashPrefix('!'); 16 | 17 | $routeProvider.otherwise({redirectTo: '/dashboard'}); 18 | } 19 | ]); 20 | 21 | softEtherAdminApp.controller('HeaderController', function ($scope, $http) { 22 | var self = this; 23 | self.loading = true; 24 | 25 | $http.get('api/server/info').then(function(response) { 26 | var data = response.data; 27 | self.hostName = data['Host Name']; 28 | self.version = data['Version']; 29 | }, function (reason) { 30 | console.log(reason); 31 | self.hostName = '-unknown-'; 32 | }).finally(function () { 33 | self.loading = false; 34 | }); 35 | 36 | // console.log($scope); 37 | }); 38 | 39 | softEtherAdminApp.controller('SidebarController', function ($scope, $location) { 40 | var self = this; 41 | self.navData = [ 42 | { 43 | isActive: false, 44 | path: 'dashboard', 45 | icon: 'pe-7s-graph', 46 | text: 'Dashboard' 47 | }, 48 | { 49 | isActive: false, 50 | path: 'server', 51 | icon: 'pe-7s-server', 52 | text: 'Server' 53 | }, 54 | { 55 | isActive: false, 56 | path: 'hubs', 57 | icon: 'pe-7s-share', 58 | text: 'Hubs' 59 | }, 60 | { 61 | isActive: false, 62 | path: 'config', 63 | icon: 'pe-7s-config', 64 | text: 'Config' 65 | } 66 | ]; 67 | 68 | $scope.$on('$locationChangeSuccess', function(event) { 69 | var path = $location.path(); 70 | for (let i = 0; i < self.navData.length; i++) { 71 | if (path.startsWith('/' + self.navData[i].path)) { 72 | self.navData[i].isActive = true; 73 | } 74 | else { 75 | self.navData[i].isActive = false; 76 | } 77 | } 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /public/app/common/autoDataTable.template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

 {{$ctrl.title}}

5 |

{{$ctrl.category}}

6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
{{item}}
{{item}}
19 |
20 |

No data

21 |
22 |
23 | 31 |
32 |
33 |
34 |
-------------------------------------------------------------------------------- /public/app/common/common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var module = angular.module('commonModule', [ 4 | 'ngRoute' 5 | ]); 6 | 7 | /* 8 | * GENERIC COMPONENTS 9 | */ 10 | 11 | module.component('loadingIndicator', { 12 | templateUrl: 'app/common/loadingIndicator.template.html', 13 | bindings: { 14 | errorMessage: '<', 15 | isLoading: '<' 16 | }, 17 | controller: function () { 18 | var ctrl = this; 19 | 20 | // ctrl.$onInit = function () { 21 | // console.log('init'); 22 | // }; 23 | 24 | // ctrl.$onChanges = function (changes) { 25 | // console.log('CHANGE! ' + ctrl.isLoading + ' - ' + ctrl.errorMessage); 26 | // }; 27 | 28 | }/*, 29 | controllerAs: 'ctrl'*/ 30 | }); 31 | 32 | module.component('infoCard', { 33 | templateUrl: 'app/common/infoCard.template.html', 34 | bindings: { 35 | size: '@', 36 | icon: '@', 37 | title: '@', 38 | category: '@', 39 | value: '<', 40 | errorMessage: '<', 41 | loading: '<' 42 | }, 43 | controller: function () { 44 | var ctrl = this; 45 | // console.log('infoCard'); 46 | } 47 | }); 48 | 49 | module.component('keyValueTable', { 50 | templateUrl: 'app/common/keyValueTable.template.html', 51 | bindings: { 52 | data: '<', 53 | headerKey: '@', 54 | headerValue: '@', 55 | title: '@', 56 | category: '@', 57 | icon: '@', 58 | size: '@', 59 | errorMessage: '<', 60 | loading: '<' 61 | }, 62 | controller: function () { 63 | var ctrl = this; 64 | // console.log('keyValueTable'); 65 | } 66 | }); 67 | 68 | module.component('autoDataTable', { 69 | templateUrl: 'app/common/autoDataTable.template.html', 70 | bindings: { 71 | data: '<', 72 | header: '<', 73 | title: '@', 74 | category: '@', 75 | icon: '@', 76 | size: '@', 77 | errorMessage: '<', 78 | loading: '<', 79 | reloadCallback: '<' 80 | }, 81 | controller: function () { 82 | var ctrl = this; 83 | //ctrl.allowReload = false; 84 | // console.log('autoDataTable'); 85 | } 86 | }); 87 | -------------------------------------------------------------------------------- /public/app/common/infoCard.template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

 {{$ctrl.title}}

5 |

{{$ctrl.category}}

6 |
7 |
8 | 9 |

{{$ctrl.value}}

10 |
11 |
12 |
-------------------------------------------------------------------------------- /public/app/common/keyValueTable.template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

 {{$ctrl.title}}

5 |

{{$ctrl.category}}

6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
{{$ctrl.headerKey}}{{$ctrl.headerValue}}
{{key}}{{value}}
21 | 22 |
23 |
24 |
-------------------------------------------------------------------------------- /public/app/common/loadingIndicator.template.html: -------------------------------------------------------------------------------- 1 | 2 |

Error: {{$ctrl.errorMessage}}

3 | -------------------------------------------------------------------------------- /public/app/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('configModule', [ 4 | 'ngRoute' 5 | ]) 6 | 7 | .config(['$routeProvider', function($routeProvider) { 8 | $routeProvider.when('/config', { 9 | templateUrl: 'app/config/config.template.html', 10 | controller: 'configController' 11 | }); 12 | }]) 13 | 14 | .controller('configController', function($scope, $http) { 15 | var self = this; 16 | self.loading = true; 17 | self.errorMessage = null; 18 | 19 | $http.get('api/server/config').then(function (response) { 20 | self.serverConfig = response.data; 21 | }, function (reason) { 22 | console.log('error'); 23 | console.log(reason.statusText); 24 | self.errorMessage = reason.statusText; 25 | }).finally(function () { 26 | self.loading = false; 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /public/app/config/config.template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 | 8 |
9 |

Server config

10 |
11 |
12 |
13 |
14 |
15 | 16 | 17 |
18 |
19 |
20 |
21 |
22 |
23 | 24 |
25 |
-------------------------------------------------------------------------------- /public/app/dashboard/dashboard.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var module = angular.module('dashboardModule', [ 4 | 'ngRoute' 5 | ]); 6 | 7 | module.config(['$routeProvider', function ($routeProvider) { 8 | $routeProvider.when('/dashboard', { 9 | templateUrl: 'app/dashboard/dashboard.template.html', 10 | controller: 'dashboardController' 11 | }); 12 | }]); 13 | 14 | module.controller('dashboardController', function ($scope, $http) { 15 | var self = this; 16 | self.loading = true; 17 | self.errorMessage = null; 18 | 19 | // get the server status 20 | $http.get('api/server/dashboardData').then(function (response) { 21 | var serverStatusData = response.data['ServerStatusGet']; 22 | self.infoData = response.data['ServerInfoGet']; 23 | 24 | // calculate the uptime 25 | var startDateString = serverStatusData['Server Started at'].replace(/\(\w*\)./g, ''); 26 | self.uptime = getUptime(startDateString); 27 | 28 | // calculate the unicast data transferred 29 | var unicastOutData = serverStatusData['Outgoing Unicast Total Size']; 30 | var unicastInData = serverStatusData['Incoming Unicast Total Size']; 31 | self.unicastData = convertToLargestDataUnit(unicastInData) + '/' + convertToLargestDataUnit(unicastOutData); 32 | 33 | // calculate the broadcast data transferred 34 | var broadcastOutData = serverStatusData['Outgoing Broadcast Total Size']; 35 | var broadcastInData = serverStatusData['Incoming Broadcast Total Size']; 36 | self.broadcastData = convertToLargestDataUnit(broadcastInData) + '/' + convertToLargestDataUnit(broadcastOutData); 37 | 38 | self.numberOfUsers = serverStatusData['Number of Users']; 39 | self.numberOfSessions = serverStatusData['Number of Sessions']; 40 | 41 | var keysToKeep = [ 42 | 'Server Type', 43 | 'Number of Active Sockets', 44 | 'Number of MAC Address Tables', 45 | 'Number of IP Address Tables', 46 | 'Number of Groups', 47 | 'Using Client Connection Licenses (This Server)', 48 | 'Using Bridge Connection Licenses (This Server)', 49 | 'Server Started at', 50 | 'Current Time' 51 | ]; 52 | 53 | // filter the data, as we don't need everything 54 | for (var key in serverStatusData) { 55 | if (serverStatusData.hasOwnProperty(key)) { 56 | if (!keysToKeep.includes(key)) { 57 | delete serverStatusData[key]; 58 | } 59 | } 60 | } 61 | self.statusData = serverStatusData; 62 | 63 | }, function (reason) { 64 | console.log(reason); 65 | self.errorMessage = reason.statusText; 66 | }).finally(function () { 67 | self.loading = false; 68 | }); 69 | 70 | }); -------------------------------------------------------------------------------- /public/app/dashboard/dashboard.template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 |
20 | 21 | 22 | 23 |
24 |
-------------------------------------------------------------------------------- /public/app/hubs/hubs.hubdetails.template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | Select a hub from the list above! 5 |
6 |
7 |
8 |
9 |

{{$ctrl.hubName}}

10 |

Hub details

11 |
12 |
13 | 14 | 15 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 43 | 44 | 45 | 55 | 56 | 57 | 67 | 68 | 69 | 79 | 80 |
-------------------------------------------------------------------------------- /public/app/hubs/hubs.hublist.template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Hubs

4 |

The hubs registered on this server

5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
NameStatusTypeUsersGroupsSessionsMACsIPsNum LoginsLast LoginLast CommsData 
{{item["Virtual Hub Name"]}}{{item["Status"]}}{{item["Type"]}}{{item["Users"]}}{{item["Groups"]}}{{item["Sessions"]}}{{item["MAC Tables"]}}{{item["IP Tables"]}}{{item["Num Logins"]}}{{item["Last Login"]}}{{item["Last Communication"]}}{{item["transferredData"]}}
42 |
43 |
-------------------------------------------------------------------------------- /public/app/hubs/hubs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var module = angular.module('hubsModule', ['ngRoute']); 4 | 5 | module.config([ 6 | '$routeProvider', 7 | function ($routeProvider) { 8 | $routeProvider 9 | .when('/hubs', { 10 | templateUrl: 'app/hubs/hubs.template.html', 11 | controller: 'hubsController' 12 | }) 13 | .when('/hubs/:hubName', { 14 | templateUrl: 'app/hubs/hubs.template.html', 15 | controller: 'hubsController' 16 | }); 17 | } 18 | ]); 19 | 20 | module.controller('hubsController', function ($scope, $http, $location, $routeParams) { 21 | var self = this; 22 | self.hubName = $routeParams.hubName; 23 | 24 | self.showDetails = function (hubName) { 25 | $location.path('hubs/' + hubName); 26 | //self.hubName = hubName; 27 | //console.log('Selected hub: ' + hubName); 28 | }; 29 | }); 30 | 31 | module.component('hubList', { 32 | templateUrl: 'app/hubs/hubs.hublist.template.html', 33 | bindings: { 34 | callback: '<' 35 | }, 36 | controller: function ($http) { 37 | var ctrl = this; 38 | ctrl.loading = true; 39 | ctrl.errorMessage = null; 40 | 41 | $http 42 | .get('api/server/hublist') 43 | .then( 44 | function (response) { 45 | ctrl.data = response.data; 46 | // convert all the Transfer Bytes to the largest unit 47 | for (let i = 0; i < ctrl.data.length; i++) { 48 | ctrl.data[i]['transferredData'] = convertToLargestDataUnit(ctrl.data[i]['Transfer Bytes']); 49 | } 50 | }, 51 | function (reason) { 52 | console.log(reason); 53 | ctrl.errorMessage = reason.statusText; 54 | } 55 | ) 56 | .finally(function () { 57 | ctrl.loading = false; 58 | }); 59 | } 60 | }); 61 | 62 | module.component('hubDetails', { 63 | templateUrl: 'app/hubs/hubs.hubdetails.template.html', 64 | bindings: { 65 | $router: '<', 66 | hubName: '<' 67 | }, 68 | controller: function ($http) { 69 | var ctrl = this; 70 | 71 | // handle incoming hubname 72 | ctrl.$onChanges = function (changes) { 73 | if (!ctrl.hubName) { 74 | // console.log('hubName is not defined!'); 75 | return; 76 | } 77 | 78 | ctrl.loading = true; 79 | ctrl.errorMessage = null; 80 | ctrl.loading_session = true; 81 | ctrl.errorMessage_session = null; 82 | ctrl.hasData = false; 83 | 84 | $http 85 | .get('api/hub/' + ctrl.hubName + '/hubData') 86 | .then( 87 | function (response) { 88 | ctrl.statusData = response.data['StatusGet']; 89 | ctrl.accessListData = response.data['AccessList']; 90 | ctrl.accessListHeaders = getArrayHeaders(ctrl.accessListData); 91 | 92 | ctrl.userListData = response.data['UserList']; 93 | ctrl.userListHeaders = getArrayHeaders(ctrl.userListData); 94 | 95 | ctrl.groupListData = response.data['GroupList']; 96 | ctrl.groupListHeaders = getArrayHeaders(ctrl.groupListData); 97 | 98 | ctrl.macTableData = response.data['MacTable']; 99 | ctrl.ipTableData = response.data['IpTable']; 100 | 101 | ctrl.sessionListData = response.data['SessionList']; 102 | // merge the ip and the mac address into the session list 103 | mergeSessionData(ctrl); 104 | ctrl.sessionListHeaders = getArrayHeaders(ctrl.sessionListData); 105 | 106 | ctrl.hasData = true; 107 | }, 108 | function (reason) { 109 | console.log(reason); 110 | ctrl.errorMessage = ctrl.errorMessage_session = reason.statusText; 111 | } 112 | ) 113 | .finally(function () { 114 | ctrl.loading = false; 115 | ctrl.loading_session = false; 116 | }); 117 | 118 | }; // onchange 119 | 120 | ctrl.reloadSessionList = function () { 121 | ctrl.loading_session = true; 122 | $http 123 | .get('api/hub/' + ctrl.hubName + '/sessionData') 124 | .then( 125 | function (response) { 126 | ctrl.macTableData = response.data['MacTable']; 127 | ctrl.ipTableData = response.data['IpTable']; 128 | ctrl.sessionListData = response.data['SessionList']; 129 | // merge the ip and the mac address into the session list 130 | mergeSessionData(ctrl); 131 | ctrl.sessionListHeaders = getArrayHeaders(ctrl.sessionListData); 132 | 133 | // ctrl.hasData = true; 134 | }, 135 | function (reason) { 136 | console.log(reason); 137 | ctrl.errorMessage_session = reason.statusText; 138 | } 139 | ) 140 | .finally(function () { 141 | ctrl.loading_session = false; 142 | }); 143 | }; // reloadSessionList 144 | } // controller 145 | 146 | }); 147 | 148 | function mergeSessionData(ctrl) { 149 | // merge the ip and the mac address into the session list 150 | for (let i = 0; i < ctrl.sessionListData.length; i++) { 151 | delete ctrl.sessionListData[i]['Transfer Packets']; 152 | ctrl.sessionListData[i]['Transfer Bytes'] = convertToLargestDataUnit(ctrl.sessionListData[i]['Transfer Bytes']); 153 | const name = ctrl.sessionListData[i]['Session Name']; 154 | // merge the mac 155 | for (let j = 0; j < ctrl.macTableData.length; j++) { 156 | if (ctrl.macTableData[j]['Session Name'] == name) { 157 | ctrl.sessionListData[i]['MAC Address'] = ctrl.macTableData[j]['MAC Address']; 158 | } 159 | } 160 | // merge the ip 161 | for (let j = 0; j < ctrl.ipTableData.length; j++) { 162 | if (ctrl.ipTableData[j]['Session Name'] == name) { 163 | const address = ctrl.ipTableData[j]['IP Address']; 164 | 165 | if (!ctrl.sessionListData[i].hasOwnProperty('IPv4 Address')) { 166 | ctrl.sessionListData[i]['IPv4 Address'] = '-'; 167 | } 168 | if (!ctrl.sessionListData[i].hasOwnProperty('IPv6 Address')) { 169 | ctrl.sessionListData[i]['IPv6 Address'] = '-'; 170 | } 171 | 172 | if (address.includes(':')) { 173 | // ipv6 174 | ctrl.sessionListData[i]['IPv6 Address'] = address; 175 | } 176 | else { 177 | // ipv4 178 | ctrl.sessionListData[i]['IPv4 Address'] = address; 179 | } 180 | } 181 | } 182 | } 183 | } -------------------------------------------------------------------------------- /public/app/hubs/hubs.template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 |
7 | 8 |
9 | 10 |
11 | 12 |
13 |
-------------------------------------------------------------------------------- /public/app/server/server.connections.template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Connections

4 |

Active connections to the server

5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
Connection NameConnection SourceConnection StartType
{{item["Connection Name"]}}{{item["Connection Source"]}}{{item["Connection Start"]}}{{item["Type"]}}
24 |
25 |
-------------------------------------------------------------------------------- /public/app/server/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var module = angular.module('serverModule', [ 4 | 'ngRoute' 5 | ]); 6 | 7 | module.config(['$routeProvider', function ($routeProvider) { 8 | $routeProvider.when('/server', { 9 | templateUrl: 'app/server/server.template.html', 10 | controller: 'ServerController' 11 | }); 12 | }]) 13 | 14 | module.controller('ServerController', function ($scope, $http) { 15 | var self = this; 16 | self.loading_caps = true; 17 | self.errorMessage_caps = null; 18 | 19 | // get the server capabilities 20 | $http.get('api/server/caps').then(function (response) { 21 | var data = response.data; 22 | self.capsData = data; 23 | 24 | }, function (reason) { 25 | console.log(reason); 26 | self.errorMessage_caps = reason.statusText; 27 | }).finally(function () { 28 | self.loading_caps = false; 29 | }); 30 | }); 31 | 32 | module.component('connectionsList', { 33 | templateUrl: 'app/server/server.connections.template.html', 34 | bindings: {}, 35 | controller: function ($http) { 36 | var ctrl = this; 37 | ctrl.loading = true; 38 | ctrl.errorMessage = null; 39 | 40 | $http.get('api/server/connectionList').then(function (response) { 41 | ctrl.data = response.data; 42 | }, function (reason) { 43 | console.log(reason); 44 | ctrl.errorMessage = reason.statusText; 45 | }).finally(function () { 46 | ctrl.loading = false; 47 | }); 48 | } 49 | }); 50 | -------------------------------------------------------------------------------- /public/app/server/server.template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 |
7 |
8 | 9 |
10 | 11 | 13 |
14 |
-------------------------------------------------------------------------------- /public/app/utils.js: -------------------------------------------------------------------------------- 1 | function getUptime(startDateString) { 2 | // parse the date 3 | var startDate = Date.parse(startDateString); 4 | // get the difference 5 | var dateDifference = Date.now() - startDate; 6 | // get the total ms, s, m, h, d 7 | var dateData = getDateTimeTotals(dateDifference); 8 | // select the largest that is not 0 9 | if (dateData.d > 0) { 10 | return dateData.d + "d"; 11 | } else if (dateData.h > 0) { 12 | return dateData.h + "h"; 13 | } else if (dateData.m > 0) { 14 | return dateData.m + "m"; 15 | } else { 16 | return dateData.s + "s"; 17 | } 18 | } 19 | 20 | function getDateTimeTotals(date) { 21 | return { 22 | ms: Math.floor(date % 1000), 23 | s: Math.floor((date / 1000) % 60), 24 | m: Math.floor((date / 60000) % 60), 25 | h: Math.floor((date / 3600000) % 24), 26 | d: Math.floor(date / 86400000) 27 | }; 28 | } 29 | 30 | function convertToLargestDataUnit(value) { 31 | var bytes = value.replace(/\D/g, ""); // bytes 32 | var units = getDataUnits(bytes); 33 | if (units.pb > 0) { 34 | return units.pb + "P"; 35 | } else if (units.gb > 0) { 36 | return units.gb + "G"; 37 | } else if (units.mb > 0) { 38 | return units.mb + "M"; 39 | } else if (units.kb > 0) { 40 | return units.kb + "k"; 41 | } else { 42 | return units.b + "B"; 43 | } 44 | } 45 | 46 | function getDataUnits(value) { 47 | return { 48 | b: value, 49 | kb: Math.floor(value / 1024), 50 | mb: Math.floor(value / 1048576), 51 | gb: Math.floor(value / 1073741824), 52 | pb: Math.floor(value / 1099511627776) 53 | }; 54 | } 55 | 56 | function getArrayHeaders(data) { 57 | if (data.length < 1) { 58 | return []; 59 | } 60 | return Object.keys(data[0]); 61 | } -------------------------------------------------------------------------------- /public/css/pe-icon-7-stroke.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Pe-icon-7-stroke'; 3 | src:url('../fonts/Pe-icon-7-stroke.eot?d7yf1v'); 4 | src:url('../fonts/Pe-icon-7-stroke.eot?#iefixd7yf1v') format('embedded-opentype'), 5 | url('../fonts/Pe-icon-7-stroke.woff?d7yf1v') format('woff'), 6 | url('../fonts/Pe-icon-7-stroke.ttf?d7yf1v') format('truetype'), 7 | url('../fonts/Pe-icon-7-stroke.svg?d7yf1v#Pe-icon-7-stroke') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | [class^="pe-7s-"], [class*=" pe-7s-"] { 13 | display: inline-block; 14 | font-family: 'Pe-icon-7-stroke'; 15 | speak: none; 16 | font-style: normal; 17 | font-weight: normal; 18 | font-variant: normal; 19 | text-transform: none; 20 | line-height: 1; 21 | 22 | /* Better Font Rendering =========== */ 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | .pe-7s-album:before { 28 | content: "\e6aa"; 29 | } 30 | .pe-7s-arc:before { 31 | content: "\e6ab"; 32 | } 33 | .pe-7s-back-2:before { 34 | content: "\e6ac"; 35 | } 36 | .pe-7s-bandaid:before { 37 | content: "\e6ad"; 38 | } 39 | .pe-7s-car:before { 40 | content: "\e6ae"; 41 | } 42 | .pe-7s-diamond:before { 43 | content: "\e6af"; 44 | } 45 | .pe-7s-door-lock:before { 46 | content: "\e6b0"; 47 | } 48 | .pe-7s-eyedropper:before { 49 | content: "\e6b1"; 50 | } 51 | .pe-7s-female:before { 52 | content: "\e6b2"; 53 | } 54 | .pe-7s-gym:before { 55 | content: "\e6b3"; 56 | } 57 | .pe-7s-hammer:before { 58 | content: "\e6b4"; 59 | } 60 | .pe-7s-headphones:before { 61 | content: "\e6b5"; 62 | } 63 | .pe-7s-helm:before { 64 | content: "\e6b6"; 65 | } 66 | .pe-7s-hourglass:before { 67 | content: "\e6b7"; 68 | } 69 | .pe-7s-leaf:before { 70 | content: "\e6b8"; 71 | } 72 | .pe-7s-magic-wand:before { 73 | content: "\e6b9"; 74 | } 75 | .pe-7s-male:before { 76 | content: "\e6ba"; 77 | } 78 | .pe-7s-map-2:before { 79 | content: "\e6bb"; 80 | } 81 | .pe-7s-next-2:before { 82 | content: "\e6bc"; 83 | } 84 | .pe-7s-paint-bucket:before { 85 | content: "\e6bd"; 86 | } 87 | .pe-7s-pendrive:before { 88 | content: "\e6be"; 89 | } 90 | .pe-7s-photo:before { 91 | content: "\e6bf"; 92 | } 93 | .pe-7s-piggy:before { 94 | content: "\e6c0"; 95 | } 96 | .pe-7s-plugin:before { 97 | content: "\e6c1"; 98 | } 99 | .pe-7s-refresh-2:before { 100 | content: "\e6c2"; 101 | } 102 | .pe-7s-rocket:before { 103 | content: "\e6c3"; 104 | } 105 | .pe-7s-settings:before { 106 | content: "\e6c4"; 107 | } 108 | .pe-7s-shield:before { 109 | content: "\e6c5"; 110 | } 111 | .pe-7s-smile:before { 112 | content: "\e6c6"; 113 | } 114 | .pe-7s-usb:before { 115 | content: "\e6c7"; 116 | } 117 | .pe-7s-vector:before { 118 | content: "\e6c8"; 119 | } 120 | .pe-7s-wine:before { 121 | content: "\e6c9"; 122 | } 123 | .pe-7s-cloud-upload:before { 124 | content: "\e68a"; 125 | } 126 | .pe-7s-cash:before { 127 | content: "\e68c"; 128 | } 129 | .pe-7s-close:before { 130 | content: "\e680"; 131 | } 132 | .pe-7s-bluetooth:before { 133 | content: "\e68d"; 134 | } 135 | .pe-7s-cloud-download:before { 136 | content: "\e68b"; 137 | } 138 | .pe-7s-way:before { 139 | content: "\e68e"; 140 | } 141 | .pe-7s-close-circle:before { 142 | content: "\e681"; 143 | } 144 | .pe-7s-id:before { 145 | content: "\e68f"; 146 | } 147 | .pe-7s-angle-up:before { 148 | content: "\e682"; 149 | } 150 | .pe-7s-wristwatch:before { 151 | content: "\e690"; 152 | } 153 | .pe-7s-angle-up-circle:before { 154 | content: "\e683"; 155 | } 156 | .pe-7s-world:before { 157 | content: "\e691"; 158 | } 159 | .pe-7s-angle-right:before { 160 | content: "\e684"; 161 | } 162 | .pe-7s-volume:before { 163 | content: "\e692"; 164 | } 165 | .pe-7s-angle-right-circle:before { 166 | content: "\e685"; 167 | } 168 | .pe-7s-users:before { 169 | content: "\e693"; 170 | } 171 | .pe-7s-angle-left:before { 172 | content: "\e686"; 173 | } 174 | .pe-7s-user-female:before { 175 | content: "\e694"; 176 | } 177 | .pe-7s-angle-left-circle:before { 178 | content: "\e687"; 179 | } 180 | .pe-7s-up-arrow:before { 181 | content: "\e695"; 182 | } 183 | .pe-7s-angle-down:before { 184 | content: "\e688"; 185 | } 186 | .pe-7s-switch:before { 187 | content: "\e696"; 188 | } 189 | .pe-7s-angle-down-circle:before { 190 | content: "\e689"; 191 | } 192 | .pe-7s-scissors:before { 193 | content: "\e697"; 194 | } 195 | .pe-7s-wallet:before { 196 | content: "\e600"; 197 | } 198 | .pe-7s-safe:before { 199 | content: "\e698"; 200 | } 201 | .pe-7s-volume2:before { 202 | content: "\e601"; 203 | } 204 | .pe-7s-volume1:before { 205 | content: "\e602"; 206 | } 207 | .pe-7s-voicemail:before { 208 | content: "\e603"; 209 | } 210 | .pe-7s-video:before { 211 | content: "\e604"; 212 | } 213 | .pe-7s-user:before { 214 | content: "\e605"; 215 | } 216 | .pe-7s-upload:before { 217 | content: "\e606"; 218 | } 219 | .pe-7s-unlock:before { 220 | content: "\e607"; 221 | } 222 | .pe-7s-umbrella:before { 223 | content: "\e608"; 224 | } 225 | .pe-7s-trash:before { 226 | content: "\e609"; 227 | } 228 | .pe-7s-tools:before { 229 | content: "\e60a"; 230 | } 231 | .pe-7s-timer:before { 232 | content: "\e60b"; 233 | } 234 | .pe-7s-ticket:before { 235 | content: "\e60c"; 236 | } 237 | .pe-7s-target:before { 238 | content: "\e60d"; 239 | } 240 | .pe-7s-sun:before { 241 | content: "\e60e"; 242 | } 243 | .pe-7s-study:before { 244 | content: "\e60f"; 245 | } 246 | .pe-7s-stopwatch:before { 247 | content: "\e610"; 248 | } 249 | .pe-7s-star:before { 250 | content: "\e611"; 251 | } 252 | .pe-7s-speaker:before { 253 | content: "\e612"; 254 | } 255 | .pe-7s-signal:before { 256 | content: "\e613"; 257 | } 258 | .pe-7s-shuffle:before { 259 | content: "\e614"; 260 | } 261 | .pe-7s-shopbag:before { 262 | content: "\e615"; 263 | } 264 | .pe-7s-share:before { 265 | content: "\e616"; 266 | } 267 | .pe-7s-server:before { 268 | content: "\e617"; 269 | } 270 | .pe-7s-search:before { 271 | content: "\e618"; 272 | } 273 | .pe-7s-film:before { 274 | content: "\e6a5"; 275 | } 276 | .pe-7s-science:before { 277 | content: "\e619"; 278 | } 279 | .pe-7s-disk:before { 280 | content: "\e6a6"; 281 | } 282 | .pe-7s-ribbon:before { 283 | content: "\e61a"; 284 | } 285 | .pe-7s-repeat:before { 286 | content: "\e61b"; 287 | } 288 | .pe-7s-refresh:before { 289 | content: "\e61c"; 290 | } 291 | .pe-7s-add-user:before { 292 | content: "\e6a9"; 293 | } 294 | .pe-7s-refresh-cloud:before { 295 | content: "\e61d"; 296 | } 297 | .pe-7s-paperclip:before { 298 | content: "\e69c"; 299 | } 300 | .pe-7s-radio:before { 301 | content: "\e61e"; 302 | } 303 | .pe-7s-note2:before { 304 | content: "\e69d"; 305 | } 306 | .pe-7s-print:before { 307 | content: "\e61f"; 308 | } 309 | .pe-7s-network:before { 310 | content: "\e69e"; 311 | } 312 | .pe-7s-prev:before { 313 | content: "\e620"; 314 | } 315 | .pe-7s-mute:before { 316 | content: "\e69f"; 317 | } 318 | .pe-7s-power:before { 319 | content: "\e621"; 320 | } 321 | .pe-7s-medal:before { 322 | content: "\e6a0"; 323 | } 324 | .pe-7s-portfolio:before { 325 | content: "\e622"; 326 | } 327 | .pe-7s-like2:before { 328 | content: "\e6a1"; 329 | } 330 | .pe-7s-plus:before { 331 | content: "\e623"; 332 | } 333 | .pe-7s-left-arrow:before { 334 | content: "\e6a2"; 335 | } 336 | .pe-7s-play:before { 337 | content: "\e624"; 338 | } 339 | .pe-7s-key:before { 340 | content: "\e6a3"; 341 | } 342 | .pe-7s-plane:before { 343 | content: "\e625"; 344 | } 345 | .pe-7s-joy:before { 346 | content: "\e6a4"; 347 | } 348 | .pe-7s-photo-gallery:before { 349 | content: "\e626"; 350 | } 351 | .pe-7s-pin:before { 352 | content: "\e69b"; 353 | } 354 | .pe-7s-phone:before { 355 | content: "\e627"; 356 | } 357 | .pe-7s-plug:before { 358 | content: "\e69a"; 359 | } 360 | .pe-7s-pen:before { 361 | content: "\e628"; 362 | } 363 | .pe-7s-right-arrow:before { 364 | content: "\e699"; 365 | } 366 | .pe-7s-paper-plane:before { 367 | content: "\e629"; 368 | } 369 | .pe-7s-delete-user:before { 370 | content: "\e6a7"; 371 | } 372 | .pe-7s-paint:before { 373 | content: "\e62a"; 374 | } 375 | .pe-7s-bottom-arrow:before { 376 | content: "\e6a8"; 377 | } 378 | .pe-7s-notebook:before { 379 | content: "\e62b"; 380 | } 381 | .pe-7s-note:before { 382 | content: "\e62c"; 383 | } 384 | .pe-7s-next:before { 385 | content: "\e62d"; 386 | } 387 | .pe-7s-news-paper:before { 388 | content: "\e62e"; 389 | } 390 | .pe-7s-musiclist:before { 391 | content: "\e62f"; 392 | } 393 | .pe-7s-music:before { 394 | content: "\e630"; 395 | } 396 | .pe-7s-mouse:before { 397 | content: "\e631"; 398 | } 399 | .pe-7s-more:before { 400 | content: "\e632"; 401 | } 402 | .pe-7s-moon:before { 403 | content: "\e633"; 404 | } 405 | .pe-7s-monitor:before { 406 | content: "\e634"; 407 | } 408 | .pe-7s-micro:before { 409 | content: "\e635"; 410 | } 411 | .pe-7s-menu:before { 412 | content: "\e636"; 413 | } 414 | .pe-7s-map:before { 415 | content: "\e637"; 416 | } 417 | .pe-7s-map-marker:before { 418 | content: "\e638"; 419 | } 420 | .pe-7s-mail:before { 421 | content: "\e639"; 422 | } 423 | .pe-7s-mail-open:before { 424 | content: "\e63a"; 425 | } 426 | .pe-7s-mail-open-file:before { 427 | content: "\e63b"; 428 | } 429 | .pe-7s-magnet:before { 430 | content: "\e63c"; 431 | } 432 | .pe-7s-loop:before { 433 | content: "\e63d"; 434 | } 435 | .pe-7s-look:before { 436 | content: "\e63e"; 437 | } 438 | .pe-7s-lock:before { 439 | content: "\e63f"; 440 | } 441 | .pe-7s-lintern:before { 442 | content: "\e640"; 443 | } 444 | .pe-7s-link:before { 445 | content: "\e641"; 446 | } 447 | .pe-7s-like:before { 448 | content: "\e642"; 449 | } 450 | .pe-7s-light:before { 451 | content: "\e643"; 452 | } 453 | .pe-7s-less:before { 454 | content: "\e644"; 455 | } 456 | .pe-7s-keypad:before { 457 | content: "\e645"; 458 | } 459 | .pe-7s-junk:before { 460 | content: "\e646"; 461 | } 462 | .pe-7s-info:before { 463 | content: "\e647"; 464 | } 465 | .pe-7s-home:before { 466 | content: "\e648"; 467 | } 468 | .pe-7s-help2:before { 469 | content: "\e649"; 470 | } 471 | .pe-7s-help1:before { 472 | content: "\e64a"; 473 | } 474 | .pe-7s-graph3:before { 475 | content: "\e64b"; 476 | } 477 | .pe-7s-graph2:before { 478 | content: "\e64c"; 479 | } 480 | .pe-7s-graph1:before { 481 | content: "\e64d"; 482 | } 483 | .pe-7s-graph:before { 484 | content: "\e64e"; 485 | } 486 | .pe-7s-global:before { 487 | content: "\e64f"; 488 | } 489 | .pe-7s-gleam:before { 490 | content: "\e650"; 491 | } 492 | .pe-7s-glasses:before { 493 | content: "\e651"; 494 | } 495 | .pe-7s-gift:before { 496 | content: "\e652"; 497 | } 498 | .pe-7s-folder:before { 499 | content: "\e653"; 500 | } 501 | .pe-7s-flag:before { 502 | content: "\e654"; 503 | } 504 | .pe-7s-filter:before { 505 | content: "\e655"; 506 | } 507 | .pe-7s-file:before { 508 | content: "\e656"; 509 | } 510 | .pe-7s-expand1:before { 511 | content: "\e657"; 512 | } 513 | .pe-7s-exapnd2:before { 514 | content: "\e658"; 515 | } 516 | .pe-7s-edit:before { 517 | content: "\e659"; 518 | } 519 | .pe-7s-drop:before { 520 | content: "\e65a"; 521 | } 522 | .pe-7s-drawer:before { 523 | content: "\e65b"; 524 | } 525 | .pe-7s-download:before { 526 | content: "\e65c"; 527 | } 528 | .pe-7s-display2:before { 529 | content: "\e65d"; 530 | } 531 | .pe-7s-display1:before { 532 | content: "\e65e"; 533 | } 534 | .pe-7s-diskette:before { 535 | content: "\e65f"; 536 | } 537 | .pe-7s-date:before { 538 | content: "\e660"; 539 | } 540 | .pe-7s-cup:before { 541 | content: "\e661"; 542 | } 543 | .pe-7s-culture:before { 544 | content: "\e662"; 545 | } 546 | .pe-7s-crop:before { 547 | content: "\e663"; 548 | } 549 | .pe-7s-credit:before { 550 | content: "\e664"; 551 | } 552 | .pe-7s-copy-file:before { 553 | content: "\e665"; 554 | } 555 | .pe-7s-config:before { 556 | content: "\e666"; 557 | } 558 | .pe-7s-compass:before { 559 | content: "\e667"; 560 | } 561 | .pe-7s-comment:before { 562 | content: "\e668"; 563 | } 564 | .pe-7s-coffee:before { 565 | content: "\e669"; 566 | } 567 | .pe-7s-cloud:before { 568 | content: "\e66a"; 569 | } 570 | .pe-7s-clock:before { 571 | content: "\e66b"; 572 | } 573 | .pe-7s-check:before { 574 | content: "\e66c"; 575 | } 576 | .pe-7s-chat:before { 577 | content: "\e66d"; 578 | } 579 | .pe-7s-cart:before { 580 | content: "\e66e"; 581 | } 582 | .pe-7s-camera:before { 583 | content: "\e66f"; 584 | } 585 | .pe-7s-call:before { 586 | content: "\e670"; 587 | } 588 | .pe-7s-calculator:before { 589 | content: "\e671"; 590 | } 591 | .pe-7s-browser:before { 592 | content: "\e672"; 593 | } 594 | .pe-7s-box2:before { 595 | content: "\e673"; 596 | } 597 | .pe-7s-box1:before { 598 | content: "\e674"; 599 | } 600 | .pe-7s-bookmarks:before { 601 | content: "\e675"; 602 | } 603 | .pe-7s-bicycle:before { 604 | content: "\e676"; 605 | } 606 | .pe-7s-bell:before { 607 | content: "\e677"; 608 | } 609 | .pe-7s-battery:before { 610 | content: "\e678"; 611 | } 612 | .pe-7s-ball:before { 613 | content: "\e679"; 614 | } 615 | .pe-7s-back:before { 616 | content: "\e67a"; 617 | } 618 | .pe-7s-attention:before { 619 | content: "\e67b"; 620 | } 621 | .pe-7s-anchor:before { 622 | content: "\e67c"; 623 | } 624 | .pe-7s-albums:before { 625 | content: "\e67d"; 626 | } 627 | .pe-7s-alarm:before { 628 | content: "\e67e"; 629 | } 630 | .pe-7s-airplay:before { 631 | content: "\e67f"; 632 | } 633 | -------------------------------------------------------------------------------- /public/fonts/Pe-icon-7-stroke.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notisrac/SoftEtherAdmin/a088a5824e7eaaa110db7f1e5625c0f6b14e05c7/public/fonts/Pe-icon-7-stroke.eot -------------------------------------------------------------------------------- /public/fonts/Pe-icon-7-stroke.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notisrac/SoftEtherAdmin/a088a5824e7eaaa110db7f1e5625c0f6b14e05c7/public/fonts/Pe-icon-7-stroke.ttf -------------------------------------------------------------------------------- /public/fonts/Pe-icon-7-stroke.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notisrac/SoftEtherAdmin/a088a5824e7eaaa110db7f1e5625c0f6b14e05c7/public/fonts/Pe-icon-7-stroke.woff -------------------------------------------------------------------------------- /public/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notisrac/SoftEtherAdmin/a088a5824e7eaaa110db7f1e5625c0f6b14e05c7/public/img/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | SoftEher Admin 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 | 81 | 82 | 83 |
84 | 102 | 103 | 104 |
105 |
106 | 107 | 114 | 115 |
116 |
117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 155 | 156 | -------------------------------------------------------------------------------- /public/js/bootstrap-notify.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | 5 | Creative Tim Modifications 6 | 7 | Lines: 239, 240 was changed from top: 5px to top: 50% and we added margin-top: -13px. In this way the close button will be aligned vertically 8 | Line:242 - modified when the icon is set, we add the class "alert-with-icon", so there will be enough space for the icon. 9 | 10 | 11 | 12 | 13 | */ 14 | 15 | 16 | /* 17 | * Project: Bootstrap Notify = v3.1.5 18 | * Description: Turns standard Bootstrap alerts into "Growl-like" notifications. 19 | * Author: Mouse0270 aka Robert McIntosh 20 | * License: MIT License 21 | * Website: https://github.com/mouse0270/bootstrap-growl 22 | */ 23 | 24 | /* global define:false, require: false, jQuery:false */ 25 | 26 | (function (factory) { 27 | if (typeof define === 'function' && define.amd) { 28 | // AMD. Register as an anonymous module. 29 | define(['jquery'], factory); 30 | } else if (typeof exports === 'object') { 31 | // Node/CommonJS 32 | factory(require('jquery')); 33 | } else { 34 | // Browser globals 35 | factory(jQuery); 36 | } 37 | }(function ($) { 38 | // Create the defaults once 39 | var defaults = { 40 | element: 'body', 41 | position: null, 42 | type: "info", 43 | allow_dismiss: true, 44 | allow_duplicates: true, 45 | newest_on_top: false, 46 | showProgressbar: false, 47 | placement: { 48 | from: "top", 49 | align: "right" 50 | }, 51 | offset: 20, 52 | spacing: 10, 53 | z_index: 1031, 54 | delay: 5000, 55 | timer: 1000, 56 | url_target: '_blank', 57 | mouse_over: null, 58 | animate: { 59 | enter: 'animated fadeInDown', 60 | exit: 'animated fadeOutUp' 61 | }, 62 | onShow: null, 63 | onShown: null, 64 | onClose: null, 65 | onClosed: null, 66 | icon_type: 'class', 67 | template: '' 68 | }; 69 | 70 | String.format = function () { 71 | var str = arguments[0]; 72 | for (var i = 1; i < arguments.length; i++) { 73 | str = str.replace(RegExp("\\{" + (i - 1) + "\\}", "gm"), arguments[i]); 74 | } 75 | return str; 76 | }; 77 | 78 | function isDuplicateNotification(notification) { 79 | var isDupe = false; 80 | 81 | $('[data-notify="container"]').each(function (i, el) { 82 | var $el = $(el); 83 | var title = $el.find('[data-notify="title"]').text().trim(); 84 | var message = $el.find('[data-notify="message"]').html().trim(); 85 | 86 | // The input string might be different than the actual parsed HTML string! 87 | // (
vs
for example) 88 | // So we have to force-parse this as HTML here! 89 | var isSameTitle = title === $("
" + notification.settings.content.title + "
").html().trim(); 90 | var isSameMsg = message === $("
" + notification.settings.content.message + "
").html().trim(); 91 | var isSameType = $el.hasClass('alert-' + notification.settings.type); 92 | 93 | if (isSameTitle && isSameMsg && isSameType) { 94 | //we found the dupe. Set the var and stop checking. 95 | isDupe = true; 96 | } 97 | return !isDupe; 98 | }); 99 | 100 | return isDupe; 101 | } 102 | 103 | function Notify(element, content, options) { 104 | // Setup Content of Notify 105 | var contentObj = { 106 | content: { 107 | message: typeof content === 'object' ? content.message : content, 108 | title: content.title ? content.title : '', 109 | icon: content.icon ? content.icon : '', 110 | url: content.url ? content.url : '#', 111 | target: content.target ? content.target : '-' 112 | } 113 | }; 114 | 115 | options = $.extend(true, {}, contentObj, options); 116 | this.settings = $.extend(true, {}, defaults, options); 117 | this._defaults = defaults; 118 | if (this.settings.content.target === "-") { 119 | this.settings.content.target = this.settings.url_target; 120 | } 121 | this.animations = { 122 | start: 'webkitAnimationStart oanimationstart MSAnimationStart animationstart', 123 | end: 'webkitAnimationEnd oanimationend MSAnimationEnd animationend' 124 | }; 125 | 126 | if (typeof this.settings.offset === 'number') { 127 | this.settings.offset = { 128 | x: this.settings.offset, 129 | y: this.settings.offset 130 | }; 131 | } 132 | 133 | //if duplicate messages are not allowed, then only continue if this new message is not a duplicate of one that it already showing 134 | if (this.settings.allow_duplicates || (!this.settings.allow_duplicates && !isDuplicateNotification(this))) { 135 | this.init(); 136 | } 137 | } 138 | 139 | $.extend(Notify.prototype, { 140 | init: function () { 141 | var self = this; 142 | 143 | this.buildNotify(); 144 | if (this.settings.content.icon) { 145 | this.setIcon(); 146 | } 147 | if (this.settings.content.url != "#") { 148 | this.styleURL(); 149 | } 150 | this.styleDismiss(); 151 | this.placement(); 152 | this.bind(); 153 | 154 | this.notify = { 155 | $ele: this.$ele, 156 | update: function (command, update) { 157 | var commands = {}; 158 | if (typeof command === "string") { 159 | commands[command] = update; 160 | } else { 161 | commands = command; 162 | } 163 | for (var cmd in commands) { 164 | switch (cmd) { 165 | case "type": 166 | this.$ele.removeClass('alert-' + self.settings.type); 167 | this.$ele.find('[data-notify="progressbar"] > .progress-bar').removeClass('progress-bar-' + self.settings.type); 168 | self.settings.type = commands[cmd]; 169 | this.$ele.addClass('alert-' + commands[cmd]).find('[data-notify="progressbar"] > .progress-bar').addClass('progress-bar-' + commands[cmd]); 170 | break; 171 | case "icon": 172 | var $icon = this.$ele.find('[data-notify="icon"]'); 173 | if (self.settings.icon_type.toLowerCase() === 'class') { 174 | $icon.removeClass(self.settings.content.icon).addClass(commands[cmd]); 175 | } else { 176 | if (!$icon.is('img')) { 177 | $icon.find('img'); 178 | } 179 | $icon.attr('src', commands[cmd]); 180 | } 181 | break; 182 | case "progress": 183 | var newDelay = self.settings.delay - (self.settings.delay * (commands[cmd] / 100)); 184 | this.$ele.data('notify-delay', newDelay); 185 | this.$ele.find('[data-notify="progressbar"] > div').attr('aria-valuenow', commands[cmd]).css('width', commands[cmd] + '%'); 186 | break; 187 | case "url": 188 | this.$ele.find('[data-notify="url"]').attr('href', commands[cmd]); 189 | break; 190 | case "target": 191 | this.$ele.find('[data-notify="url"]').attr('target', commands[cmd]); 192 | break; 193 | default: 194 | this.$ele.find('[data-notify="' + cmd + '"]').html(commands[cmd]); 195 | } 196 | } 197 | var posX = this.$ele.outerHeight() + parseInt(self.settings.spacing) + parseInt(self.settings.offset.y); 198 | self.reposition(posX); 199 | }, 200 | close: function () { 201 | self.close(); 202 | } 203 | }; 204 | 205 | }, 206 | buildNotify: function () { 207 | var content = this.settings.content; 208 | this.$ele = $(String.format(this.settings.template, this.settings.type, content.title, content.message, content.url, content.target)); 209 | this.$ele.attr('data-notify-position', this.settings.placement.from + '-' + this.settings.placement.align); 210 | if (!this.settings.allow_dismiss) { 211 | this.$ele.find('[data-notify="dismiss"]').css('display', 'none'); 212 | } 213 | if ((this.settings.delay <= 0 && !this.settings.showProgressbar) || !this.settings.showProgressbar) { 214 | this.$ele.find('[data-notify="progressbar"]').remove(); 215 | } 216 | }, 217 | setIcon: function () { 218 | 219 | this.$ele.addClass('alert-with-icon'); 220 | 221 | if (this.settings.icon_type.toLowerCase() === 'class') { 222 | this.$ele.find('[data-notify="icon"]').addClass(this.settings.content.icon); 223 | } else { 224 | if (this.$ele.find('[data-notify="icon"]').is('img')) { 225 | this.$ele.find('[data-notify="icon"]').attr('src', this.settings.content.icon); 226 | } else { 227 | this.$ele.find('[data-notify="icon"]').append('Notify Icon'); 228 | } 229 | } 230 | }, 231 | styleDismiss: function () { 232 | this.$ele.find('[data-notify="dismiss"]').css({ 233 | position: 'absolute', 234 | right: '10px', 235 | top: '50%', 236 | marginTop: '-13px', 237 | zIndex: this.settings.z_index + 2 238 | }); 239 | }, 240 | styleURL: function () { 241 | this.$ele.find('[data-notify="url"]').css({ 242 | backgroundImage: 'url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)', 243 | height: '100%', 244 | left: 0, 245 | position: 'absolute', 246 | top: 0, 247 | width: '100%', 248 | zIndex: this.settings.z_index + 1 249 | }); 250 | }, 251 | placement: function () { 252 | var self = this, 253 | offsetAmt = this.settings.offset.y, 254 | css = { 255 | display: 'inline-block', 256 | margin: '0px auto', 257 | position: this.settings.position ? this.settings.position : (this.settings.element === 'body' ? 'fixed' : 'absolute'), 258 | transition: 'all .5s ease-in-out', 259 | zIndex: this.settings.z_index 260 | }, 261 | hasAnimation = false, 262 | settings = this.settings; 263 | 264 | $('[data-notify-position="' + this.settings.placement.from + '-' + this.settings.placement.align + '"]:not([data-closing="true"])').each(function () { 265 | offsetAmt = Math.max(offsetAmt, parseInt($(this).css(settings.placement.from)) + parseInt($(this).outerHeight()) + parseInt(settings.spacing)); 266 | }); 267 | if (this.settings.newest_on_top === true) { 268 | offsetAmt = this.settings.offset.y; 269 | } 270 | css[this.settings.placement.from] = offsetAmt + 'px'; 271 | 272 | switch (this.settings.placement.align) { 273 | case "left": 274 | case "right": 275 | css[this.settings.placement.align] = this.settings.offset.x + 'px'; 276 | break; 277 | case "center": 278 | css.left = 0; 279 | css.right = 0; 280 | break; 281 | } 282 | this.$ele.css(css).addClass(this.settings.animate.enter); 283 | $.each(Array('webkit-', 'moz-', 'o-', 'ms-', ''), function (index, prefix) { 284 | self.$ele[0].style[prefix + 'AnimationIterationCount'] = 1; 285 | }); 286 | 287 | $(this.settings.element).append(this.$ele); 288 | 289 | if (this.settings.newest_on_top === true) { 290 | offsetAmt = (parseInt(offsetAmt) + parseInt(this.settings.spacing)) + this.$ele.outerHeight(); 291 | this.reposition(offsetAmt); 292 | } 293 | 294 | if ($.isFunction(self.settings.onShow)) { 295 | self.settings.onShow.call(this.$ele); 296 | } 297 | 298 | this.$ele.one(this.animations.start, function () { 299 | hasAnimation = true; 300 | }).one(this.animations.end, function () { 301 | if ($.isFunction(self.settings.onShown)) { 302 | self.settings.onShown.call(this); 303 | } 304 | }); 305 | 306 | setTimeout(function () { 307 | if (!hasAnimation) { 308 | if ($.isFunction(self.settings.onShown)) { 309 | self.settings.onShown.call(this); 310 | } 311 | } 312 | }, 600); 313 | }, 314 | bind: function () { 315 | var self = this; 316 | 317 | this.$ele.find('[data-notify="dismiss"]').on('click', function () { 318 | self.close(); 319 | }); 320 | 321 | this.$ele.mouseover(function () { 322 | $(this).data('data-hover', "true"); 323 | }).mouseout(function () { 324 | $(this).data('data-hover', "false"); 325 | }); 326 | this.$ele.data('data-hover', "false"); 327 | 328 | if (this.settings.delay > 0) { 329 | self.$ele.data('notify-delay', self.settings.delay); 330 | var timer = setInterval(function () { 331 | var delay = parseInt(self.$ele.data('notify-delay')) - self.settings.timer; 332 | if ((self.$ele.data('data-hover') === 'false' && self.settings.mouse_over === "pause") || self.settings.mouse_over != "pause") { 333 | var percent = ((self.settings.delay - delay) / self.settings.delay) * 100; 334 | self.$ele.data('notify-delay', delay); 335 | self.$ele.find('[data-notify="progressbar"] > div').attr('aria-valuenow', percent).css('width', percent + '%'); 336 | } 337 | if (delay <= -(self.settings.timer)) { 338 | clearInterval(timer); 339 | self.close(); 340 | } 341 | }, self.settings.timer); 342 | } 343 | }, 344 | close: function () { 345 | var self = this, 346 | posX = parseInt(this.$ele.css(this.settings.placement.from)), 347 | hasAnimation = false; 348 | 349 | this.$ele.data('closing', 'true').addClass(this.settings.animate.exit); 350 | self.reposition(posX); 351 | 352 | if ($.isFunction(self.settings.onClose)) { 353 | self.settings.onClose.call(this.$ele); 354 | } 355 | 356 | this.$ele.one(this.animations.start, function () { 357 | hasAnimation = true; 358 | }).one(this.animations.end, function () { 359 | $(this).remove(); 360 | if ($.isFunction(self.settings.onClosed)) { 361 | self.settings.onClosed.call(this); 362 | } 363 | }); 364 | 365 | setTimeout(function () { 366 | if (!hasAnimation) { 367 | self.$ele.remove(); 368 | if (self.settings.onClosed) { 369 | self.settings.onClosed(self.$ele); 370 | } 371 | } 372 | }, 600); 373 | }, 374 | reposition: function (posX) { 375 | var self = this, 376 | notifies = '[data-notify-position="' + this.settings.placement.from + '-' + this.settings.placement.align + '"]:not([data-closing="true"])', 377 | $elements = this.$ele.nextAll(notifies); 378 | if (this.settings.newest_on_top === true) { 379 | $elements = this.$ele.prevAll(notifies); 380 | } 381 | $elements.each(function () { 382 | $(this).css(self.settings.placement.from, posX); 383 | posX = (parseInt(posX) + parseInt(self.settings.spacing)) + $(this).outerHeight(); 384 | }); 385 | } 386 | }); 387 | 388 | $.notify = function (content, options) { 389 | var plugin = new Notify(this, content, options); 390 | return plugin.notify; 391 | }; 392 | $.notifyDefaults = function (options) { 393 | defaults = $.extend(true, {}, defaults, options); 394 | return defaults; 395 | }; 396 | $.notifyClose = function (command) { 397 | if (typeof command === "undefined" || command === "all") { 398 | $('[data-notify]').find('[data-notify="dismiss"]').trigger('click'); 399 | } else { 400 | $('[data-notify-position="' + command + '"]').find('[data-notify="dismiss"]').trigger('click'); 401 | } 402 | }; 403 | 404 | })); 405 | -------------------------------------------------------------------------------- /public/js/bootstrap-select.js: -------------------------------------------------------------------------------- 1 | !function($) { 2 | var Selectpicker = function(element, options, e) { 3 | if (e ) { 4 | e.stopPropagation(); 5 | e.preventDefault(); 6 | } 7 | this.$element = $(element); 8 | this.$newElement = null; 9 | this.button = null; 10 | 11 | //Merge defaults, options and data-attributes to make our options 12 | this.options = $.extend({}, $.fn.selectpicker.defaults, this.$element.data(), typeof options == 'object' && options); 13 | 14 | //If we have no title yet, check the attribute 'title' (this is missed by jq as its not a data-attribute 15 | if(this.options.title==null) 16 | this.options.title = this.$element.attr('title'); 17 | 18 | //Expose public methods 19 | this.val = Selectpicker.prototype.val; 20 | this.render = Selectpicker.prototype.render; 21 | this.init(); 22 | }; 23 | 24 | Selectpicker.prototype = { 25 | 26 | constructor: Selectpicker, 27 | 28 | init: function (e) { 29 | var _this = this; 30 | this.$element.hide(); 31 | this.multiple = this.$element.prop('multiple'); 32 | 33 | 34 | var classList = this.$element.attr('class') !== undefined ? this.$element.attr('class').split(/\s+/) : ''; 35 | var id = this.$element.attr('id'); 36 | this.$element.after( this.createView() ); 37 | this.$newElement = this.$element.next('.select'); 38 | var select = this.$newElement; 39 | var menu = this.$newElement.find('.dropdown-menu'); 40 | var menuArrow = this.$newElement.find('.dropdown-arrow'); 41 | var menuA = menu.find('li > a'); 42 | var liHeight = select.addClass('open').find('.dropdown-menu li > a').outerHeight(); 43 | select.removeClass('open'); 44 | var divHeight = menu.find('li .divider').outerHeight(true); 45 | var selectOffset_top = this.$newElement.offset().top; 46 | var size = 0; 47 | var menuHeight = 0; 48 | var selectHeight = this.$newElement.outerHeight(); 49 | this.button = this.$newElement.find('> button'); 50 | if (id !== undefined) { 51 | this.button.attr('id', id); 52 | $('label[for="' + id + '"]').click(function(){ select.find('button#'+id).focus(); }) 53 | } 54 | for (var i = 0; i < classList.length; i++) { 55 | if(classList[i] != 'selectpicker') { 56 | this.$newElement.addClass(classList[i]); 57 | } 58 | } 59 | //If we are multiple, then add the show-tick class by default 60 | if(this.multiple) { 61 | this.$newElement.addClass('select-multiple'); 62 | } 63 | this.button.addClass(this.options.style); 64 | menu.addClass(this.options.menuStyle); 65 | menuArrow.addClass(function() { 66 | if (_this.options.menuStyle) { 67 | return _this.options.menuStyle.replace('dropdown-', 'dropdown-arrow-'); 68 | } 69 | }); 70 | this.checkDisabled(); 71 | this.checkTabIndex(); 72 | this.clickListener(); 73 | var menuPadding = parseInt(menu.css('padding-top')) + parseInt(menu.css('padding-bottom')) + parseInt(menu.css('border-top-width')) + parseInt(menu.css('border-bottom-width')); 74 | if (this.options.size == 'auto') { 75 | 76 | // Creative Tim Changes: We changed the regular function made in bootstrap-select with this function so the getSize() will not be triggered one million times per second while you scroll. 77 | 78 | var getSize = debounce(function() { 79 | var selectOffset_top_scroll = selectOffset_top - $(window).scrollTop(); 80 | var windowHeight = $(window).innerHeight(); 81 | var menuExtras = menuPadding + parseInt(menu.css('margin-top')) + parseInt(menu.css('margin-bottom')) + 2; 82 | var selectOffset_bot = windowHeight - selectOffset_top_scroll - selectHeight - menuExtras; 83 | menuHeight = selectOffset_bot; 84 | if (select.hasClass('dropup')) { 85 | menuHeight = selectOffset_top_scroll - menuExtras; 86 | } 87 | //limit menuHeight to 300px to have a smooth transition with cubic bezier on dropdown 88 | if(menuHeight >= 300){ 89 | menuHeight = 300; 90 | } 91 | 92 | menu.css({'max-height' : menuHeight + 'px', 'overflow-y' : 'auto', 'min-height' : liHeight * 3 + 'px'}); 93 | 94 | }, 50); 95 | 96 | getSize; 97 | $(window).on('scroll', getSize); 98 | $(window).on('resize', getSize); 99 | 100 | if (window.MutationObserver) { 101 | new MutationObserver(getSize).observe(this.$element.get(0), { 102 | childList: true 103 | }); 104 | } else { 105 | this.$element.bind('DOMNodeInserted', getSize); 106 | } 107 | } else if (this.options.size && this.options.size != 'auto' && menu.find('li').length > this.options.size) { 108 | var optIndex = menu.find("li > *").filter(':not(.divider)').slice(0,this.options.size).last().parent().index(); 109 | var divLength = menu.find("li").slice(0,optIndex + 1).find('.divider').length; 110 | menuHeight = liHeight*this.options.size + divLength*divHeight + menuPadding; 111 | menu.css({'max-height' : menuHeight + 'px', 'overflow-y' : 'scroll'}); 112 | //console.log('sunt in if'); 113 | } 114 | 115 | // Listen for updates to the DOM and re render... (Use Mutation Observer when availiable) 116 | if (window.MutationObserver) { 117 | new MutationObserver($.proxy(this.reloadLi, this)).observe(this.$element.get(0), { 118 | childList: true 119 | }); 120 | } else { 121 | this.$element.bind('DOMNodeInserted', $.proxy(this.reloadLi, this)); 122 | } 123 | 124 | this.render(); 125 | }, 126 | 127 | createDropdown: function() { 128 | var drop = 129 | "
" + 130 | "" + 134 | "" + 135 | "" + 137 | "
"; 138 | 139 | return $(drop); 140 | }, 141 | 142 | 143 | createView: function() { 144 | var $drop = this.createDropdown(); 145 | var $li = this.createLi(); 146 | $drop.find('ul').append($li); 147 | return $drop; 148 | }, 149 | 150 | reloadLi: function() { 151 | //Remove all children. 152 | this.destroyLi(); 153 | //Re build 154 | $li = this.createLi(); 155 | this.$newElement.find('ul').append( $li ); 156 | //render view 157 | this.render(); 158 | }, 159 | 160 | destroyLi:function() { 161 | this.$newElement.find('li').remove(); 162 | }, 163 | 164 | createLi: function() { 165 | 166 | var _this = this; 167 | var _li = []; 168 | var _liA = []; 169 | var _liHtml = ''; 170 | 171 | this.$element.find('option').each(function(){ 172 | _li.push($(this).text()); 173 | }); 174 | 175 | this.$element.find('option').each(function(index) { 176 | //Get the class and text for the option 177 | var optionClass = $(this).attr("class") !== undefined ? $(this).attr("class") : ''; 178 | var text = $(this).text(); 179 | var subtext = $(this).data('subtext') !== undefined ? ''+$(this).data('subtext')+'' : ''; 180 | 181 | //Append any subtext to the main text. 182 | text+=subtext; 183 | 184 | if ($(this).parent().is('optgroup') && $(this).data('divider') != true) { 185 | if ($(this).index() == 0) { 186 | //Get the opt group label 187 | var label = $(this).parent().attr('label'); 188 | var labelSubtext = $(this).parent().data('subtext') !== undefined ? ''+$(this).parent().data('subtext')+'' : ''; 189 | label += labelSubtext; 190 | 191 | if ($(this)[0].index != 0) { 192 | _liA.push( 193 | '
'+ 194 | '
'+label+'
'+ 195 | _this.createA(text, "opt " + optionClass ) 196 | ); 197 | } else { 198 | _liA.push( 199 | '
'+label+'
'+ 200 | _this.createA(text, "opt " + optionClass )); 201 | } 202 | } else { 203 | _liA.push( _this.createA(text, "opt " + optionClass ) ); 204 | } 205 | } else if ($(this).data('divider') == true) { 206 | _liA.push('
'); 207 | } else if ($(this).data('hidden') == true) { 208 | _liA.push(''); 209 | } else { 210 | _liA.push( _this.createA(text, optionClass ) ); 211 | } 212 | }); 213 | 214 | if (_li.length > 0) { 215 | for (var i = 0; i < _li.length; i++) { 216 | var $option = this.$element.find('option').eq(i); 217 | _liHtml += "
  • " + _liA[i] + "
  • "; 218 | } 219 | } 220 | 221 | //If we dont have a selected item, and we dont have a title, select the first element so something is set in the button 222 | if(this.$element.find('option:selected').length==0 && !_this.options.title) { 223 | this.$element.find('option').eq(0).prop('selected', true).attr('selected', 'selected'); 224 | } 225 | 226 | return $(_liHtml); 227 | }, 228 | 229 | createA:function(test, classes) { 230 | return '' + 231 | '' + test + '' + 232 | ''; 233 | 234 | }, 235 | 236 | render:function() { 237 | var _this = this; 238 | 239 | //Set width of select 240 | if (this.options.width == 'auto') { 241 | var ulWidth = this.$newElement.find('.dropdown-menu').css('width'); 242 | this.$newElement.css('width',ulWidth); 243 | } else if (this.options.width && this.options.width != 'auto') { 244 | this.$newElement.css('width',this.options.width); 245 | } 246 | 247 | //Update the LI to match the SELECT 248 | this.$element.find('option').each(function(index) { 249 | _this.setDisabled(index, $(this).is(':disabled') || $(this).parent().is(':disabled') ); 250 | _this.setSelected(index, $(this).is(':selected') ); 251 | }); 252 | 253 | 254 | 255 | var selectedItems = this.$element.find('option:selected').map(function(index,value) { 256 | if($(this).attr('title')!=undefined) { 257 | return $(this).attr('title'); 258 | } else { 259 | return $(this).text(); 260 | } 261 | }).toArray(); 262 | 263 | //Convert all the values into a comma delimited string 264 | var title = selectedItems.join(", "); 265 | 266 | //If this is multi select, and the selectText type is count, the show 1 of 2 selected etc.. 267 | if(_this.multiple && _this.options.selectedTextFormat.indexOf('count') > -1) { 268 | var max = _this.options.selectedTextFormat.split(">"); 269 | if( (max.length>1 && selectedItems.length > max[1]) || (max.length==1 && selectedItems.length>=2)) { 270 | title = selectedItems.length +' of ' + this.$element.find('option').length + ' selected'; 271 | } 272 | } 273 | 274 | //If we dont have a title, then use the default, or if nothing is set at all, use the not selected text 275 | if(!title) { 276 | title = _this.options.title != undefined ? _this.options.title : _this.options.noneSelectedText; 277 | } 278 | 279 | this.$element.next('.select').find('.filter-option').html( title ); 280 | }, 281 | 282 | 283 | 284 | setSelected:function(index, selected) { 285 | if(selected) { 286 | this.$newElement.find('li').eq(index).addClass('selected'); 287 | } else { 288 | this.$newElement.find('li').eq(index).removeClass('selected'); 289 | } 290 | }, 291 | 292 | setDisabled:function(index, disabled) { 293 | if(disabled) { 294 | this.$newElement.find('li').eq(index).addClass('disabled'); 295 | } else { 296 | this.$newElement.find('li').eq(index).removeClass('disabled'); 297 | } 298 | }, 299 | 300 | checkDisabled: function() { 301 | if (this.$element.is(':disabled')) { 302 | this.button.addClass('disabled'); 303 | this.button.click(function(e) { 304 | e.preventDefault(); 305 | }); 306 | } 307 | }, 308 | 309 | checkTabIndex: function() { 310 | if (this.$element.is('[tabindex]')) { 311 | var tabindex = this.$element.attr("tabindex"); 312 | this.button.attr('tabindex', tabindex); 313 | } 314 | }, 315 | 316 | clickListener: function() { 317 | var _this = this; 318 | 319 | $('body').on('touchstart.dropdown', '.dropdown-menu', function (e) { e.stopPropagation(); }); 320 | 321 | 322 | 323 | this.$newElement.on('click', 'li a', function(e){ 324 | var clickedIndex = $(this).parent().index(), 325 | $this = $(this).parent(), 326 | $select = $this.parents('.select'); 327 | 328 | 329 | //Dont close on multi choice menu 330 | if(_this.multiple) { 331 | e.stopPropagation(); 332 | } 333 | 334 | e.preventDefault(); 335 | 336 | //Dont run if we have been disabled 337 | if ($select.prev('select').not(':disabled') && !$(this).parent().hasClass('disabled')){ 338 | //Deselect all others if not multi select box 339 | if (!_this.multiple) { 340 | $select.prev('select').find('option').removeAttr('selected'); 341 | $select.prev('select').find('option').eq(clickedIndex).prop('selected', true).attr('selected', 'selected'); 342 | } 343 | //Else toggle the one we have chosen if we are multi selet. 344 | else { 345 | var selected = $select.prev('select').find('option').eq(clickedIndex).prop('selected'); 346 | 347 | if(selected) { 348 | $select.prev('select').find('option').eq(clickedIndex).removeAttr('selected'); 349 | } else { 350 | $select.prev('select').find('option').eq(clickedIndex).prop('selected', true).attr('selected', 'selected'); 351 | } 352 | } 353 | 354 | 355 | $select.find('.filter-option').html($this.text()); 356 | $select.find('button').focus(); 357 | 358 | // Trigger select 'change' 359 | $select.prev('select').trigger('change'); 360 | } 361 | 362 | }); 363 | 364 | this.$newElement.on('click', 'li.disabled a, li dt, li .divider', function(e) { 365 | e.preventDefault(); 366 | e.stopPropagation(); 367 | $select = $(this).parent().parents('.select'); 368 | $select.find('button').focus(); 369 | }); 370 | 371 | this.$element.on('change', function(e) { 372 | _this.render(); 373 | }); 374 | }, 375 | 376 | val:function(value) { 377 | 378 | if(value!=undefined) { 379 | this.$element.val( value ); 380 | 381 | this.$element.trigger('change'); 382 | return this.$element; 383 | } else { 384 | return this.$element.val(); 385 | } 386 | } 387 | 388 | }; 389 | 390 | $.fn.selectpicker = function(option, event) { 391 | //get the args of the outer function.. 392 | var args = arguments; 393 | var value; 394 | var chain = this.each(function () { 395 | var $this = $(this), 396 | data = $this.data('selectpicker'), 397 | options = typeof option == 'object' && option; 398 | 399 | if (!data) { 400 | $this.data('selectpicker', (data = new Selectpicker(this, options, event))); 401 | } else { 402 | for(var i in option) { 403 | data[i]=option[i]; 404 | } 405 | } 406 | 407 | if (typeof option == 'string') { 408 | //Copy the value of option, as once we shift the arguments 409 | //it also shifts the value of option. 410 | property = option; 411 | if(data[property] instanceof Function) { 412 | [].shift.apply(args); 413 | value = data[property].apply(data, args); 414 | } else { 415 | value = data[property]; 416 | } 417 | } 418 | }); 419 | 420 | if(value!=undefined) { 421 | return value; 422 | } else { 423 | return chain; 424 | } 425 | }; 426 | 427 | $.fn.selectpicker.defaults = { 428 | style: null, 429 | size: 'auto', 430 | title: null, 431 | selectedTextFormat : 'values', 432 | noneSelectedText : 'Nothing selected', 433 | width: null, 434 | menuStyle: null, 435 | toggleSize: null 436 | } 437 | 438 | }(window.jQuery); 439 | -------------------------------------------------------------------------------- /public/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.7 (http://getbootstrap.com) 3 | * Copyright 2011-2016 Twitter, Inc. 4 | * Licensed under the MIT license 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1||b[0]>3)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){if(a(b.target).is(this))return b.handleObj.handler.apply(this,arguments)}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.7",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a("#"===f?[]:f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.7",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c).prop(c,!0)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c).prop(c,!1))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target).closest(".btn");b.call(d,"toggle"),a(c.target).is('input[type="radio"], input[type="checkbox"]')||(c.preventDefault(),d.is("input,button")?d.trigger("focus"):d.find("input:visible,button:visible").first().trigger("focus"))}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.7",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));if(!(a>this.$items.length-1||a<0))return this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){if(!this.sliding)return this.slide("next")},c.prototype.prev=function(){if(!this.sliding)return this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.7",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger(a.Event("hidden.bs.dropdown",f)))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.7",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger(a.Event("shown.bs.dropdown",h))}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&jdocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth
    ',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);if(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),!c.isInStateTrue())return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null,a.$element=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;!e&&/destroy|hide/.test(b)||(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.7",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.7",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.7",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return e=a-d&&"bottom"},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); 8 | -------------------------------------------------------------------------------- /public/js/light-bootstrap-dashboard.js: -------------------------------------------------------------------------------- 1 | /*! 2 | 3 | ========================================================= 4 | * Light Bootstrap Dashboard - v1.4.0 5 | ========================================================= 6 | 7 | * Product Page: http://www.creative-tim.com/product/light-bootstrap-dashboard 8 | * Copyright 2017 Creative Tim (http://www.creative-tim.com) 9 | * Licensed under MIT (https://github.com/creativetimofficial/light-bootstrap-dashboard/blob/master/LICENSE.md) 10 | 11 | ========================================================= 12 | 13 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 14 | 15 | */ 16 | 17 | var searchVisible = 0; 18 | var transparent = true; 19 | 20 | var transparentDemo = true; 21 | var fixedTop = false; 22 | 23 | var navbar_initialized = false; 24 | 25 | $(document).ready(function(){ 26 | window_width = $(window).width(); 27 | 28 | // check if there is an image set for the sidebar's background 29 | lbd.checkSidebarImage(); 30 | 31 | // Init navigation toggle for small screens 32 | lbd.initRightMenu(); 33 | 34 | // Activate the tooltips 35 | $('[rel="tooltip"]').tooltip(); 36 | 37 | $('.form-control').on("focus", function(){ 38 | $(this).parent('.input-group').addClass("input-group-focus"); 39 | }).on("blur", function(){ 40 | $(this).parent(".input-group").removeClass("input-group-focus"); 41 | }); 42 | 43 | // Fixes sub-nav not working as expected on IOS 44 | $('body').on('touchstart.dropdown', '.dropdown-menu', function (e) { e.stopPropagation(); }); 45 | }); 46 | 47 | $(document).on('click', '.navbar-toggle', function(){ 48 | $toggle = $(this); 49 | 50 | if(lbd.misc.navbar_menu_visible == 1) { 51 | $('html').removeClass('nav-open'); 52 | lbd.misc.navbar_menu_visible = 0; 53 | $('#bodyClick').remove(); 54 | setTimeout(function(){ 55 | $toggle.removeClass('toggled'); 56 | }, 550); 57 | } else { 58 | setTimeout(function(){ 59 | $toggle.addClass('toggled'); 60 | }, 580); 61 | div = '
    '; 62 | $(div).appendTo('body').click(function() { 63 | $('html').removeClass('nav-open'); 64 | lbd.misc.navbar_menu_visible = 0; 65 | setTimeout(function(){ 66 | $toggle.removeClass('toggled'); 67 | $('#bodyClick').remove(); 68 | }, 550); 69 | }); 70 | 71 | $('html').addClass('nav-open'); 72 | lbd.misc.navbar_menu_visible = 1; 73 | } 74 | }); 75 | 76 | $(window).on('resize', function(){ 77 | if(navbar_initialized){ 78 | lbd.initRightMenu(); 79 | navbar_initialized = true; 80 | } 81 | }); 82 | 83 | lbd = { 84 | misc:{ 85 | navbar_menu_visible: 0 86 | }, 87 | 88 | checkSidebarImage: function(){ 89 | $sidebar = $('.sidebar'); 90 | image_src = $sidebar.data('image'); 91 | 92 | if(image_src !== undefined){ 93 | sidebar_container = '