mirror of
https://github.com/IrosTheBeggar/mStream.git
synced 2025-10-27 07:31:02 +00:00
984 lines
43 KiB
HTML
984 lines
43 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<link rel="shortcut icon" type="image/png" href="images/favicon.png"/>
|
||
<title>mStream Express Server</title>
|
||
|
||
<script src="js/materialize.min.js"></script>
|
||
<link href="css/materialize.min.css" rel="stylesheet">
|
||
|
||
<link href="css/index3.css" rel="stylesheet">
|
||
<script src="js/vue.js"></script>
|
||
|
||
<script src="js/izi-toast.min.js"></script>
|
||
<link href="css/izi-toast.min.css" rel="stylesheet">
|
||
|
||
<link href="fonts/jura.css" rel="stylesheet">
|
||
</head>
|
||
|
||
<body>
|
||
<div class="modal">
|
||
<div id="switcherModal">
|
||
<component :key="componentKey" v-bind:is="currentViewModal">
|
||
</component>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="main">
|
||
<div class="main-left-col">
|
||
<!-- Buttons -->
|
||
<div class="left-nav-menu-header">
|
||
Server
|
||
</div>
|
||
<ul class="left-nav-menu">
|
||
<li id="nav-directories" class="left-nav-button waves-effect waves-purple nav-selected">
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" height="28"><path fill="#FFA000" d="M38 12H22l-4-4H8c-2.2 0-4 1.8-4 4v24c0 2.2 1.8 4 4 4h31c1.7 0 3-1.3 3-3V16c0-2.2-1.8-4-4-4z"/><path fill="#FFCA28" d="M42.2 18H15.3c-1.9 0-3.6 1.4-3.9 3.3L8 40h31.7c1.9 0 3.6-1.4 3.9-3.3l2.5-14c.5-2.4-1.4-4.7-3.9-4.7z"/></svg>
|
||
<span>Directories</span>
|
||
</li>
|
||
<li id="nav-users" class="left-nav-button waves-effect waves-purple">
|
||
<svg xmlns="http://www.w3.org/2000/svg" height="28" viewBox="200 200 800 800"><path fill="#FFF" d="M671.027 410.286c31.749 19.852 54.013 53.574 58.044 92.642 12.928 6.092 27.325 9.578 42.596 9.578 55.593 0 100.645-45.073 100.645-100.73 0-55.591-45.052-100.706-100.645-100.706-55.152.087-99.807 44.283-100.64 99.216m-65.998 206.146c55.611 0 100.749-45.093 100.749-100.705 0-55.568-45.138-100.665-100.749-100.665-55.569 0-100.662 45.097-100.662 100.665 0 55.612 45.092 100.705 100.662 100.705m42.706 6.883H562.3c-71.084 0-128.886 57.823-128.886 128.886v104.541l.218 1.623 7.232 2.255c67.816 21.213 126.781 28.225 175.295 28.225 94.746 0 149.683-26.955 153.076-28.728l6.707-3.375h.723V752.201c0-71.063-57.827-128.886-128.93-128.886M814.33 519.32h-84.754c-.965 33.965-15.406 64.487-38.305 86.465 63.217 18.8 109.431 77.392 109.431 146.613v32.211c83.705-3.089 131.951-26.823 135.152-28.442l6.707-3.42h.744V648.273c0-71.106-57.826-128.953-128.975-128.953m-385.958-6.814c19.655 0 37.997-5.742 53.556-15.537 4.907-32.188 22.172-60.344 46.827-79.498.108-1.882.283-3.726.283-5.611 0-55.631-45.094-100.704-100.666-100.704-55.633 0-100.751 45.073-100.751 100.704.001 55.573 45.118 100.646 100.751 100.646m90.433 93.279c-22.766-21.846-37.249-52.261-38.279-85.942-3.159-.216-6.248-.523-9.468-.523h-85.455c-71.083 0-128.929 57.847-128.929 128.953v104.474l.26 1.644 7.188 2.301c54.474 17.026 103.075 24.829 145.234 27.238v-31.532c-.001-69.221 46.233-127.77 109.449-146.613"/></svg>
|
||
<span>Users</span>
|
||
</li>
|
||
<li id="nav-network" class="left-nav-button waves-effect waves-purple">
|
||
<svg xmlns="http://www.w3.org/2000/svg" height="28" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path fill="#8c9eff" d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z"/></svg>
|
||
<span>Network</span>
|
||
</li>
|
||
<li id="nav-security" class="left-nav-button waves-effect waves-purple">
|
||
<svg xmlns="http://www.w3.org/2000/svg" height="28" viewBox="0 0 460 460"><path d="M360.228,386.747L230,460L99.772,386.747c-27.804-15.639-45.01-45.06-45.01-76.96 V21.905L230,0l175.238,21.905v287.882C405.238,341.687,388.032,371.107,360.228,386.747z" fill="#86c867"/><path d="M335.565,357.061L230,416.442l-105.565-59.38 c-22.538-12.678-36.486-36.526-36.486-62.385V54.762L230,37.006l142.051,17.756v239.915 C372.051,320.535,358.103,344.384,335.565,357.061z" fill="#5e9b3e"/><path d="M230,460l130.228-73.253c27.803-15.64,45.01-45.06,45.01-76.96V21.905 L230,0v37.005l142.051,17.756v239.915c0,25.859-13.948,49.707-36.486,62.385L230,416.442V460z" opacity=".7" fill="#86c867"/><path d="M230,98.571c-30.244,0-54.762,24.518-54.762,54.762 c0,22.454,13.519,41.74,32.857,50.192V222.2h9.701c4.308,0,7.8,3.492,7.8,7.8s-3.492,7.8-7.8,7.8h-9.701v17.257h9.701 c4.308,0,7.8,3.492,7.8,7.8c0,4.308-3.492,7.8-7.8,7.8h-9.701v17.257h9.701c4.308,0,7.8,3.492,7.8,7.8c0,4.308-3.492,7.8-7.8,7.8 h-9.701v25.057L230,339.524l21.905-10.952V203.525c19.338-8.452,32.857-27.739,32.857-50.192 C284.762,123.089,260.244,98.571,230,98.571z M230,157.167c-8.166,0-14.786-6.62-14.786-14.786c0-8.166,6.62-14.786,14.786-14.786 s14.786,6.62,14.786,14.786C244.786,150.547,238.166,157.167,230,157.167z" fill="#ecf0f1"/></svg>
|
||
<span>Security</span>
|
||
</li>
|
||
<!-- <li id="nav-settings" class="left-nav-button waves-effect waves-purple">
|
||
<svg height="28" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M490.51 294.933V216.32l-57.766-11.093c-3.883-13.594-9.354-26.549-16.092-38.657l32.701-48.337-55.602-55.587-48.244 32.663c-12.309-6.885-25.492-12.177-39.338-16.109l-11.043-57.14h-78.614l-11.049 57.194c-13.895 3.956-27.123 9.4-39.463 16.338l-48.069-32.61-55.589 55.562 32.726 48.312c-6.765 12.204-12.277 25.253-16.142 38.959L21.49 216.841v78.613l57.659 11.078c3.917 13.661 9.426 26.676 16.228 38.834l-32.932 48.67 55.601 55.59 48.83-33.061c12.103 6.719 25.048 11.981 38.629 15.852l11.099 57.524h78.614l11.096-57.531c13.684-3.894 26.717-9.275 38.896-16.063l48.832 33.101 55.588-55.574-33.072-48.836c6.805-12.221 12.383-25.298 16.279-39.032l57.673-11.073m-234.535 83.084c-67.441 0-122.115-54.673-122.115-122.114s54.674-122.115 122.115-122.115S378.09 188.461 378.09 255.903s-54.674 122.114-122.115 122.114" fill="#FFF"/><path d="M254.297 378v.006c.439.006.877.01 1.316.01-.291-.002-.324-.011-1.316-.016m1.379-244.211c-.461 0-.92.004-1.379.011v.005c1.016-.004 1.062-.014 1.379-.016m.299-.001l-.086.001c67.402.047 122.113 54.702 122.113 122.114 0 67.405-54.744 122.055-122.135 122.114h.107c67.441 0 122.115-54.673 122.115-122.114s-54.673-122.115-122.114-122.115m137.8-71.143l-.014.009 55.592 55.579-32.701 48.337c6.738 12.108 12.209 25.063 16.092 38.657l57.766 11.093v78.613-78.613l-57.766-11.093a182.588 182.588 0 0 0-16.006-38.657l32.66-48.337-55.623-55.588m-98.648-40.587h-40.83 40.83l11.043 57.14c13.846 3.933 27.029 9.225 39.338 16.109l.006-.003c-12.307-6.884-25.494-12.174-39.338-16.106l-11.049-57.14" fill="#FFF"/><path d="M295.127 22.058h-40.83V133.8c.459-.007.918-.011 1.379-.011l.125-.001.088.001.086-.001c67.441 0 122.115 54.673 122.115 122.115s-54.674 122.114-122.115 122.114h-.216l-.145-.001c-.439 0-.877-.004-1.316-.01v111.936h40.92l11.096-57.531c13.684-3.894 26.717-9.275 38.896-16.063l48.832 33.101 55.588-55.574-33.072-48.836c6.805-12.221 12.383-25.298 16.279-39.032l57.674-11.073v-78.613l-57.766-11.093c-3.883-13.594-9.354-26.549-16.092-38.657l32.701-48.337-55.592-55.579-48.244 32.654-.004-.003-.006.003c-12.309-6.885-25.492-12.177-39.338-16.109l-11.043-57.142" fill="#FFF"/><path d="M256.072 111.928c-79.505 0-143.957 64.451-143.957 143.956 0 79.503 64.452 143.955 143.957 143.955 79.504 0 143.955-64.452 143.955-143.955 0-79.505-64.451-143.956-143.955-143.956zm0 217.641c-40.696 0-73.687-32.99-73.687-73.686 0-40.697 32.991-73.687 73.687-73.687 40.695 0 73.686 32.989 73.686 73.687 0 40.696-32.99 73.686-73.686 73.686z" fill="#FFF"/></svg>
|
||
<span>Settings</span>
|
||
</li> -->
|
||
</ul>
|
||
|
||
<div class="left-nav-menu-header">
|
||
Other
|
||
</div>
|
||
<ul class="left-nav-menu">
|
||
<li id="nav-about" class="left-nav-button waves-effect waves-light">
|
||
<svg xmlns="http://www.w3.org/2000/svg" height="28" viewBox="0 -1 24 24" fill="#fff"><path d="M4 2c-1.093 0-2 .907-2 2v18.406l1.719-1.687L6.438 18H20c1.093 0 2-.907 2-2V4c0-1.093-.907-2-2-2H4zm0 2h16v12H5.594l-.313.281L4 17.563V4zm7 2v2h2V6h-2zm0 3v5h2V9h-2z"/></svg>
|
||
<span>About</span>
|
||
</li>
|
||
</ul>
|
||
|
||
<!-- Boot Server Button -->
|
||
<div class="boot-server-button-wrapper">
|
||
<div class="boot-server-flex-wrapper">
|
||
<label class="autoboot-label">
|
||
<input id="boot-server-checkbox" type="checkbox" checked="checked" />
|
||
<span>Boot on startup</span>
|
||
</label>
|
||
<a id="boot-server-button" class="waves-effect waves-light btn blue accent-3">Boot Server</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="switcherMain" class="main-right-col">
|
||
<component v-bind:is="currentViewMain">
|
||
</component>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const remote = require('electron').remote;
|
||
const {ipcRenderer} = require('electron');
|
||
const app = remote.app;
|
||
const dialog = remote.require('electron').dialog;
|
||
const internalIp = require('internal-ip');
|
||
const publicIpMod = require('public-ip');
|
||
const path = require('path');
|
||
const fs = require('fs');
|
||
const Login = require('../modules/login');
|
||
const shell = require('electron').shell
|
||
|
||
var loadJson = {};
|
||
var configFile = path.join(app.getPath('userData'), 'save/server-config.json');
|
||
var editThisUser;
|
||
var bootFlag = false;
|
||
var publicIp;
|
||
|
||
(async () => {
|
||
publicIp = await publicIpMod.v4();
|
||
})();
|
||
|
||
// Open a tags in OS browser
|
||
document.addEventListener('click', function (event) {
|
||
if (event.target.tagName === 'A' && event.target.href.startsWith('http')) {
|
||
event.preventDefault()
|
||
shell.openExternal(event.target.href)
|
||
}
|
||
})
|
||
|
||
try {
|
||
if (fs.statSync(configFile).isFile()) {
|
||
loadJson = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
||
}
|
||
} catch(error) {
|
||
console.log('Failed To Load JSON');
|
||
}
|
||
|
||
if (!loadJson.users || typeof loadJson.users !== 'object') {
|
||
loadJson.users = {};
|
||
}
|
||
|
||
if (!loadJson.folders || typeof loadJson.users !== 'object') {
|
||
loadJson.folders = {};
|
||
}
|
||
|
||
if (!loadJson.ssl || typeof loadJson.ssl !== 'object') {
|
||
loadJson.ssl = {};
|
||
}
|
||
|
||
if (!loadJson.port) {
|
||
loadJson.port = 3000;
|
||
}
|
||
|
||
function hashPassword(password) {
|
||
return new Promise((resolve, reject) => {
|
||
Login.hashPassword(password, (salt, hashedPassword, err) => {
|
||
if (err) {
|
||
// return callback(false, err);
|
||
return reject('Failed to hash password');
|
||
}
|
||
resolve({salt, hashPassword: Buffer.from(hashedPassword).toString('hex')});
|
||
});
|
||
});
|
||
}
|
||
|
||
window.onload = function () {
|
||
// Initialize Modal
|
||
var modalInstance = M.Modal.init(document.querySelectorAll('.modal'), { endingTop: '20%' });
|
||
|
||
Vue.component('folder-accordion', {
|
||
data: function() {
|
||
return {
|
||
instances: null,
|
||
directories: loadJson.folders,
|
||
resetFlag: false,
|
||
closeOverride: () => {
|
||
if (this.resetFlag) {
|
||
this.$parent.componentKey = !this.$parent.componentKey;
|
||
}
|
||
}
|
||
};
|
||
},
|
||
template: '\
|
||
<ul class="z-depth-1 collapsible-folders">\
|
||
<li v-for="(value, key, index) in directories">\
|
||
<div v-on:click="toggleOptions(index, $event)" class="collapsible-header">\
|
||
<div class="accordion-header-left">\
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="100%"><path fill="#FFA000" d="M38 12H22l-4-4H8c-2.2 0-4 1.8-4 4v24c0 2.2 1.8 4 4 4h31c1.7 0 3-1.3 3-3V16c0-2.2-1.8-4-4-4z"/><path fill="#FFCA28" d="M42.2 18H15.3c-1.9 0-3.6 1.4-3.9 3.3L8 40h31.7c1.9 0 3.6-1.4 3.9-3.3l2.5-14c.5-2.4-1.4-4.7-3.9-4.7z"/></svg>\
|
||
</div>\
|
||
<div class="accordion-header-right">\
|
||
<div><b>{{key}}</b></div>\
|
||
<div>{{value.root}}</div>\
|
||
</div>\
|
||
</div>\
|
||
<div class="collapsible-body clearfix">\
|
||
<div class="folder-button-group">\
|
||
<a v-on:click="changeDirectory(value, key, index)" class="waves-effect waves-light btn">Change Directory</a>\
|
||
<a v-on:click="deleteFolder(value, key, index)" class="waves-effect waves-light btn red">\
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="100%" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path fill="#FFF" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zm2.46-7.12l1.41-1.41L12 12.59l2.12-2.12 1.41 1.41L13.41 14l2.12 2.12-1.41 1.41L12 15.41l-2.12 2.12-1.41-1.41L10.59 14l-2.13-2.12zM15.5 4l-1-1h-5l-1 1H5v2h14V4z"/><path fill="none" d="M0 0h24v24H0z"/></svg>\
|
||
</a>\
|
||
</div>\
|
||
</div>\
|
||
</li>\
|
||
</ul>',
|
||
mounted: function () {
|
||
this.instances = M.Collapsible.init(document.querySelectorAll('.collapsible-folders'), { onCloseEnd: this.closeOverride });
|
||
},
|
||
beforeDestroy: function() {
|
||
this.instances[0].destroy();
|
||
},
|
||
methods: {
|
||
changeDirectory: function(value, key, index) {
|
||
dialog.showOpenDialog({properties: [ 'openDirectory']}, (selectedDirectory) => {
|
||
if(selectedDirectory == null ){
|
||
return;
|
||
}
|
||
loadJson.folders[key] = { root: selectedDirectory[0] };
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
this.resetFlag = true;
|
||
this.instances[0].close(index);
|
||
});
|
||
},
|
||
deleteFolder: function(value, key, index) {
|
||
iziToast.question({
|
||
timeout: 20000,
|
||
close: false,
|
||
overlayClose: true,
|
||
overlay: true,
|
||
displayMode: 'once',
|
||
id: 'question',
|
||
zindex: 99999,
|
||
title: "Delete <b>'" + value.root + "'</b>?",
|
||
position: 'center',
|
||
buttons: [
|
||
['<button><b>Delete</b></button>', (instance, toast) => {
|
||
delete loadJson.folders[key];
|
||
if(loadJson.users) {
|
||
Object.keys(loadJson.users).forEach(user => {
|
||
loadJson.users[user].vpaths = loadJson.users[user].vpaths.filter(e => {
|
||
return ![key].includes(e);
|
||
});
|
||
});
|
||
}
|
||
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
this.resetFlag = true;
|
||
this.instances[0].close(index);
|
||
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
|
||
}, true],
|
||
['<button>Go Back</button>', (instance, toast) => {
|
||
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
|
||
}],
|
||
]
|
||
});
|
||
},
|
||
toggleOptions: function(index, el) {
|
||
if(el.target.classList.contains('btn')){
|
||
return;
|
||
}
|
||
|
||
if(el.currentTarget.parentElement.classList.contains('active')){
|
||
this.instances[0].close(index);
|
||
return;
|
||
}
|
||
|
||
this.instances[0].open(index);
|
||
}
|
||
}
|
||
});
|
||
|
||
const foldersView = Vue.component('folders-view', {
|
||
data() {
|
||
return {
|
||
componentKey: false, // Flip this value to force re-render the folder accordion thing
|
||
dirName: '' // for the input field
|
||
};
|
||
},
|
||
template: '\
|
||
<div>\
|
||
<div class="row">\
|
||
<div class="col s12">\
|
||
<div class="card">\
|
||
<div class="section-header">Add Folder</div>\
|
||
<form id="choose-directory-form" class="choose-directory-form" @submit.prevent="addFolderDialog">\
|
||
<div class="input-field directory-name-field">\
|
||
<input @blur="maybeResetForm()" pattern="[a-zA-Z0-9-]+" v-model="dirName" id="add-directory-name" required type="text" class="validate">\
|
||
<label for="add-directory-name">Display Name</label>\
|
||
<span class="helper-text" >No special characters</span>\
|
||
</div>\
|
||
<button class="btn green waves-effect waves-light select-folder-button" type="submit">\
|
||
Select Folder\
|
||
</button>\
|
||
</form>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
<div class="row">\
|
||
<div class="col s12">\
|
||
<div class="section-header">Your Folders:</div>\
|
||
<template>\
|
||
<folder-accordion :key="componentKey" />\
|
||
</template>\
|
||
</div>\
|
||
</div>\
|
||
</div>',
|
||
methods: {
|
||
maybeResetForm: function() {
|
||
if (this.dirName === '') {
|
||
document.getElementById("choose-directory-form").reset();
|
||
}
|
||
},
|
||
addFolderDialog: function (event) {
|
||
dialog.showOpenDialog({properties: [ 'openDirectory']}, (selectedDirectory) => {
|
||
if(selectedDirectory == null ){
|
||
return;
|
||
}
|
||
|
||
if (loadJson.folders[this.dirName]) {
|
||
iziToast.warning({
|
||
title: 'Display Name Already Exists',
|
||
message: 'Display names must be unique',
|
||
position: 'topCenter',
|
||
timeout: 3500
|
||
});
|
||
return;
|
||
}
|
||
|
||
loadJson.folders[this.dirName] = { root: selectedDirectory[0] };
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
this.componentKey = !this.componentKey;
|
||
this.dirName = '';
|
||
document.getElementById("choose-directory-form").reset();
|
||
});
|
||
},
|
||
}
|
||
});
|
||
|
||
const aboutView = Vue.component('about-view', {
|
||
template: '\
|
||
<div>\
|
||
<img class="mstream-logo" src="images/mstream-logo.svg">\
|
||
<div class="row">\
|
||
<div class="col s12">\
|
||
<div class="card">\
|
||
<div class="about-content-section">\
|
||
<div class="about-content-header">Developed By</div>\
|
||
<div class="about-content-body">Paul Sori</div>\
|
||
<div class="about-content-body">paul@mstream.io</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>',
|
||
});
|
||
|
||
Vue.component('user-accordion', {
|
||
data: function() {
|
||
return {
|
||
instances: null,
|
||
users: loadJson.users,
|
||
resetFlag: false,
|
||
closeOverride: () => {
|
||
if (this.resetFlag) {
|
||
this.$parent.componentKey = !this.$parent.componentKey;
|
||
}
|
||
}
|
||
};
|
||
},
|
||
template: '\
|
||
<ul class="z-depth-1 collapsible-folders">\
|
||
<li v-for="(value, key, index) in users">\
|
||
<div v-on:click="toggleOptions(index, $event)" class="collapsible-header">\
|
||
<div class="accordion-header-left">\
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="100%" viewBox="0 0 24 24"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/><path d="M0 0h24v24H0z" fill="none"/></svg>\
|
||
</div>\
|
||
<div class="accordion-header-right">\
|
||
<div><b>{{key}}</b></div>\
|
||
<div class="user-folders-line">\
|
||
<?xml version="1.0" encoding="utf-8"?><svg width="24" height="22" xmlns="http://www.w3.org/2000/svg" viewBox="6 6 40 40" style="enable-background:new 0 0 48 48"><path d="M16.516 20.688C16.266 21.25 12 31.906 12 31.906V17c0-.55.45-1 1-1h1.334l.35-1.052C14.857 14.427 15.45 14 16 14h5c.55 0 1.143.427 1.316.948l.35 1.052H32c.55 0 1 .45 1 1v3H17.5c-.275 0-.734.125-.984.688zM41 21H19c-.55 0-1.167.418-1.371.929l-5.258 13.143c-.204.51.079.928.629.928h22c.55 0 1.167-.418 1.371-.929l5.258-13.143c.204-.51-.079-.928-.629-.928z"/></svg>\
|
||
{{folderList(value.vpaths)}}\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
<div class="collapsible-body clearfix">\
|
||
<div class="folder-button-group">\
|
||
<a v-on:click="openUserPasswordModal(value, key, index)" class="waves-effect waves-light btn">Reset Password</a>\
|
||
<a v-on:click="openChangeFoldersModal(value, key, index)" class="waves-effect waves-light btn">Edit</a>\
|
||
<a v-on:click="deleteUser(value, key, index)" class="waves-effect waves-light btn red">\
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="100%" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path fill="#FFF" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zm2.46-7.12l1.41-1.41L12 12.59l2.12-2.12 1.41 1.41L13.41 14l2.12 2.12-1.41 1.41L12 15.41l-2.12 2.12-1.41-1.41L10.59 14l-2.13-2.12zM15.5 4l-1-1h-5l-1 1H5v2h14V4z"/><path fill="none" d="M0 0h24v24H0z"/></svg>\
|
||
</a>\
|
||
</div>\
|
||
</div>\
|
||
</li>\
|
||
</ul>',
|
||
mounted: function () {
|
||
this.instances = M.Collapsible.init(document.querySelectorAll('.collapsible-folders'), { onCloseEnd: this.closeOverride });
|
||
},
|
||
beforeDestroy: function() {
|
||
this.instances[0].destroy();
|
||
},
|
||
methods: {
|
||
folderList: function(arr) {
|
||
var returnThis = '';
|
||
arr.forEach((element) => {
|
||
returnThis += element + ', '
|
||
});
|
||
returnThis = returnThis.slice(0, -2);
|
||
return returnThis;
|
||
},
|
||
openUserPasswordModal: function(value, key, index) {
|
||
editThisUser = key;
|
||
if (vModal.$children[0]) {
|
||
vModal.componentKey = !vModal.componentKey;
|
||
}
|
||
|
||
vModal.currentViewModal = 'user-password-view';
|
||
modalInstance[0].open();
|
||
},
|
||
openChangeFoldersModal: function(value, key, index) {
|
||
editThisUser = key;
|
||
if (vModal.$children[0]) {
|
||
vModal.componentKey = !vModal.componentKey;
|
||
}
|
||
vModal.currentViewModal = 'user-folders-view';
|
||
modalInstance[0].open();
|
||
},
|
||
deleteUser: function(value, key, index) {
|
||
iziToast.question({
|
||
timeout: 20000,
|
||
close: false,
|
||
overlayClose: true,
|
||
overlay: true,
|
||
displayMode: 'once',
|
||
id: 'question',
|
||
zindex: 99999,
|
||
title: "Delete <b>'" + key + "'</b>?",
|
||
position: 'center',
|
||
buttons: [
|
||
['<button><b>Delete</b></button>', (instance, toast) => {
|
||
delete loadJson.users[key];
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
this.resetFlag = true;
|
||
this.instances[0].close(index);
|
||
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
|
||
}, true],
|
||
['<button>Go Back</button>', (instance, toast) => {
|
||
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
|
||
}],
|
||
]
|
||
});
|
||
},
|
||
toggleOptions: function(index, el) {
|
||
if(el.target.classList.contains('btn')){
|
||
return;
|
||
}
|
||
|
||
if(el.currentTarget.parentElement.classList.contains('active')){
|
||
this.instances[0].close(index);
|
||
return;
|
||
}
|
||
|
||
this.instances[0].open(index);
|
||
}
|
||
}
|
||
});
|
||
|
||
const usersView = Vue.component('users-view', {
|
||
data() {
|
||
return {
|
||
directories: loadJson.folders,
|
||
componentKey: false, // Flip this value to force re-render the folder accordion thing
|
||
newUsername: '', // for the input field
|
||
selectInstance: null,
|
||
newPassword: ''
|
||
};
|
||
},
|
||
template: '\
|
||
<div>\
|
||
<div class="row">\
|
||
<div class="col s12">\
|
||
<div class="card">\
|
||
<div class="section-header">New User</div>\
|
||
<form id="add-user-form" class="" @submit.prevent="addUser">\
|
||
<div class="row row-mod">\
|
||
<div class="input-field directory-name-field col s12 m6">\
|
||
<input @blur="maybeResetForm()" pattern="[a-zA-Z0-9-]+" v-model="newUsername" id="new-username" required type="text" class="validate">\
|
||
<label for="new-username">Username</label>\
|
||
</div>\
|
||
<div class="input-field directory-name-field col s12 m6">\
|
||
<input @blur="maybeResetForm()" v-model="newPassword" id="new-password" required type="password" class="validate">\
|
||
<label for="new-password">Password</label>\
|
||
</div>\
|
||
</div>\
|
||
<div class="row row-mod">\
|
||
<div class="input-field col s12">\
|
||
<select :disabled="Object.keys(directories).length === 0" id="new-user-dirs" multiple>\
|
||
<option disabled selected value="" v-if="Object.keys(directories).length === 0">You must add a directory before adding a user</option>\
|
||
<option v-for="(key, value) in directories" :value="value">{{ value }}</option>\
|
||
</select>\
|
||
<label for="new-user-dirs">Select User\'s Directories</label>\
|
||
</div>\
|
||
</div>\
|
||
<div class="row row-mod">\
|
||
<div class="col s12 m6">\
|
||
</div>\
|
||
<div class="col s12 m6">\
|
||
<button id="submit-add-user-form" class="btn green waves-effect waves-light col s6" type="submit">\
|
||
Add user\
|
||
</button>\
|
||
</div>\
|
||
</div>\
|
||
</form>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
<div class="row">\
|
||
<div class="col s12">\
|
||
<div class="section-header">Users:</div>\
|
||
<template>\
|
||
<user-accordion :key="componentKey" />\
|
||
</template>\
|
||
</div>\
|
||
</div>\
|
||
</div>',
|
||
mounted: function () {
|
||
this.selectInstance = M.FormSelect.init(document.querySelectorAll("#new-user-dirs"));
|
||
},
|
||
beforeDestroy: function() {
|
||
this.selectInstance[0].destroy();
|
||
},
|
||
methods: {
|
||
maybeResetForm: function() {
|
||
if (this.newUsername === '' && this.newPassword === '' && this.selectInstance[0].getSelectedValues().length === 0) {
|
||
document.getElementById("add-user-form").reset();
|
||
}
|
||
},
|
||
addUser: function (event) {
|
||
if (this.selectInstance[0].getSelectedValues().length === 0) {
|
||
iziToast.warning({
|
||
title: 'Cannot add user without a directory',
|
||
position: 'topCenter',
|
||
timeout: 3500
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (loadJson.users[this.newUsername]) {
|
||
iziToast.warning({
|
||
title: 'User Already Exists',
|
||
position: 'topCenter',
|
||
timeout: 3500
|
||
});
|
||
return;
|
||
}
|
||
|
||
hashPassword(this.newPassword).then(hashObj => {
|
||
loadJson.users[this.newUsername] = {
|
||
vpaths: this.selectInstance[0].getSelectedValues(),
|
||
password: hashObj.hashPassword,
|
||
salt: hashObj.salt
|
||
};
|
||
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
this.newPassword = '';
|
||
this.newUsername = '';
|
||
this.selectInstance[0].input.value = '';
|
||
this.selectInstance[0].el.value = "";
|
||
this.componentKey = !this.componentKey;
|
||
|
||
document.getElementById("add-user-form").reset();
|
||
});
|
||
},
|
||
}
|
||
});
|
||
|
||
const networkView = Vue.component('network-view', {
|
||
data() {
|
||
return {
|
||
lJson: loadJson,
|
||
};
|
||
},
|
||
template: '\
|
||
<div>\
|
||
<div class="row">\
|
||
<div class="col s12">\
|
||
<div class="card">\
|
||
<div class="row row-mod">\
|
||
<div class="section-header">Network Details</div>\
|
||
<div class="col s12">\
|
||
<div>Local IP: {{networkIp}}</div>\
|
||
<div>Public IP: {{pIp}}</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
<div class="row">\
|
||
<div class="col s6">\
|
||
<div class="card">\
|
||
<div class="row row-mod">\
|
||
<div class="col s12 port-form-container">\
|
||
<div class="input-field">\
|
||
<input v-model="lJson.port" @blur="updateConfig()" id="server-port" type="number" class="validate" min="1" max="65535" step="1" required />\
|
||
<label for="server-port">Port #</label>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>',
|
||
computed: {
|
||
networkIp: function () {
|
||
// `this` points to the vm instance
|
||
return internalIp.v4.sync();
|
||
},
|
||
pIp: function() {
|
||
return publicIp;
|
||
}
|
||
},
|
||
methods: {
|
||
updateConfig: function() {
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
}
|
||
}
|
||
});
|
||
|
||
const securityView = Vue.component('network-view', {
|
||
data: function() {
|
||
return {
|
||
ssl: loadJson.ssl
|
||
};
|
||
},
|
||
template: '\
|
||
<div>\
|
||
<div class="row">\
|
||
<div class="col s12">\
|
||
<div class="card">\
|
||
<div class="section-header">SSL Encryption</div>\
|
||
<div class="row row-mod">\
|
||
<div class="col s12">\
|
||
mStream comes with the ability to encrypt all traffic. This feature is not enabled by default. You must provide your own key and certificate to enable this.\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
<div class="row">\
|
||
<div class="col s12">\
|
||
<div class="card">\
|
||
<svg v-on:click="clearSSL($event)" class="remove-ssl" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#e53935" d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/><path d="M0 0h24v24H0z" fill="none"/></svg>\
|
||
<div class="section-header">SSL Credentials</div>\
|
||
<div class="row row-mod">\
|
||
<div class="col s12">\
|
||
<div class="input-field">\
|
||
<input v-model="ssl.key" v-on:click="selectKeyFile($event)" id="server-ssl-key" type="text">\
|
||
<label for="server-ssl-key">Key</label>\
|
||
</div>\
|
||
</div>\
|
||
<div class="col s12">\
|
||
<div class="input-field">\
|
||
<input v-model="ssl.cert" v-on:click="selectCertFile($event)" id="server-ssl-cert" type="text">\
|
||
<label for="server-ssl-cert">Certificate</label>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>',
|
||
mounted: function() {
|
||
M.updateTextFields();
|
||
},
|
||
methods: {
|
||
clearSSL: function() {
|
||
loadJson.ssl.key = '';
|
||
loadJson.ssl.cert = '';
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
M.updateTextFields();
|
||
},
|
||
selectKeyFile: function(el) {
|
||
dialog.showOpenDialog({properties: [ 'openFile']}, (selectedFile) => {
|
||
if(selectedFile == null ){
|
||
return;
|
||
}
|
||
el.target.value = selectedFile;
|
||
loadJson.ssl.key = selectedFile;
|
||
el.target.blur();
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
});
|
||
},
|
||
selectCertFile: function(el) {
|
||
dialog.showOpenDialog({properties: [ 'openFile']}, (selectedFile) => {
|
||
if(selectedFile == null ){
|
||
return;
|
||
}
|
||
el.target.value = selectedFile;
|
||
loadJson.ssl.cert = selectedFile;
|
||
el.target.blur();
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
});
|
||
},
|
||
}
|
||
});
|
||
|
||
const settingsView = Vue.component('settings-view', {
|
||
template: '\
|
||
<div>\
|
||
Settings\
|
||
</div>',
|
||
});
|
||
|
||
const userPasswordView = Vue.component('user-password-view', {
|
||
data() {
|
||
return {
|
||
users: loadJson.users,
|
||
currentUser: editThisUser,
|
||
resetPassword: '',
|
||
componentKey: false,
|
||
};
|
||
},
|
||
template: '\
|
||
<form @submit.prevent="updatePassword" id="reset-password-form">\
|
||
<div class="modal-content">\
|
||
<h4>Password Reset </h4>\
|
||
<p>User: <b>{{currentUser}}</b></p>\
|
||
<div class="input-field directory-name-field">\
|
||
<input @blur="maybeResetForm()" v-model="resetPassword" id="reset-password" required type="password" class="validate">\
|
||
<label for="reset-password">New Password</label>\
|
||
</div>\
|
||
</div>\
|
||
<div class="modal-footer">\
|
||
<a href="#!" class="modal-close waves-effect waves-green btn-flat">Go Back</a>\
|
||
<button id="submit-reset-password-form" class="btn green waves-effect waves-light" type="submit">\
|
||
Update Password\
|
||
</button>\
|
||
</div>\
|
||
</form>',
|
||
methods: {
|
||
maybeResetForm: function() {
|
||
if (this.resetPassword === '') {
|
||
document.getElementById("reset-password-form").reset();
|
||
}
|
||
},
|
||
updatePassword: function() {
|
||
hashPassword(this.resetPassword).then(hashObj => {
|
||
loadJson.users[this.currentUser].password = hashObj.hashPassword;
|
||
loadJson.users[this.currentUser].salt = hashObj.salt;
|
||
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
this.resetPassword = '';
|
||
|
||
document.getElementById("reset-password-form").reset();
|
||
modalInstance[0].close();
|
||
|
||
iziToast.success({
|
||
title: 'Password Updated',
|
||
position: 'topCenter',
|
||
timeout: 2500
|
||
});
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
const userFoldersView = Vue.component('user-folders-view', {
|
||
data() {
|
||
return {
|
||
directories: loadJson.folders,
|
||
users: loadJson.users,
|
||
currentUser: editThisUser,
|
||
selectInstance: null,
|
||
};
|
||
},
|
||
template: '\
|
||
<form @submit.prevent="updateFolders" id="update-folders-form">\
|
||
<div class="modal-content">\
|
||
<h4>User Folders</h4>\
|
||
<p>User: <b>{{currentUser}}</b></p>\
|
||
<select :disabled="Object.keys(directories).length === 0" id="edit-user-dirs" multiple>\
|
||
<option disabled selected value="" v-if="Object.keys(directories).length === 0">You must add a directory before adding a user</option>\
|
||
<option :selected="users[currentUser].vpaths.includes(value)" v-for="(key, value) in directories" :value="value">{{ value }}</option>\
|
||
</select>\
|
||
</div>\
|
||
<div class="modal-footer">\
|
||
<a href="#!" class="modal-close waves-effect waves-green btn-flat">Go Back</a>\
|
||
<button id="submit-reset-password-form" class="btn green waves-effect waves-light" type="submit">\
|
||
Update\
|
||
</button>\
|
||
</div>\
|
||
</form>',
|
||
mounted: function () {
|
||
this.selectInstance = M.FormSelect.init(document.querySelectorAll("#edit-user-dirs"));
|
||
},
|
||
beforeDestroy: function() {
|
||
this.selectInstance[0].destroy();
|
||
},
|
||
methods: {
|
||
maybeResetForm: function() {
|
||
|
||
},
|
||
updateFolders: function() {
|
||
loadJson.users[this.currentUser].vpaths = this.selectInstance[0].getSelectedValues();
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
modalInstance[0].close();
|
||
|
||
iziToast.success({
|
||
title: 'User Updated',
|
||
position: 'topCenter',
|
||
timeout: 2500
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
const bootView = Vue.component('boot-view', {
|
||
template: "\
|
||
<div>\
|
||
<div id='bars'>\
|
||
<div class='bar'></div>\
|
||
<div class='bar'></div>\
|
||
<div class='bar'></div>\
|
||
<div class='bar'></div>\
|
||
<div class='bar'></div>\
|
||
<div class='bar'></div>\
|
||
<div class='bar'></div>\
|
||
<div class='bar'></div>\
|
||
<div class='bar'></div>\
|
||
<div class='bar'></div>\
|
||
</div>\
|
||
<div id='boot-server-text' class='boot-server-text'>\
|
||
Booting Server\
|
||
</div>\
|
||
<div id='post-boot-content'></div>\
|
||
</div>",
|
||
});
|
||
|
||
var vModal = new Vue({
|
||
el: '#switcherModal',
|
||
components: {
|
||
'user-password-view': userPasswordView,
|
||
'user-folders-view': userFoldersView,
|
||
},
|
||
data: {
|
||
componentKey: false,
|
||
currentViewModal: false
|
||
}
|
||
});
|
||
|
||
var vm = new Vue({
|
||
el: '#switcherMain',
|
||
components: {
|
||
'folders-view': foldersView,
|
||
'about-view': aboutView,
|
||
'users-view': usersView,
|
||
'network-view': networkView,
|
||
'security-view': securityView,
|
||
'settings-view': settingsView,
|
||
'boot-view': bootView
|
||
},
|
||
data: {
|
||
currentViewMain: false
|
||
}
|
||
});
|
||
|
||
vm.currentViewMain = 'folders-view';
|
||
|
||
document.getElementById("nav-directories").onclick = function(){
|
||
if(bootFlag === true) {
|
||
return;
|
||
}
|
||
vm.currentViewMain = 'folders-view';
|
||
resetNav(this);
|
||
}
|
||
|
||
document.getElementById("nav-about").onclick = function(){
|
||
if(bootFlag === true) {
|
||
return;
|
||
}
|
||
vm.currentViewMain = 'about-view';
|
||
resetNav(this);
|
||
}
|
||
|
||
document.getElementById("nav-users").onclick = function(){
|
||
if(bootFlag === true) {
|
||
return;
|
||
}
|
||
vm.currentViewMain = 'users-view';
|
||
resetNav(this);
|
||
}
|
||
|
||
document.getElementById("nav-network").onclick = function(){
|
||
if(bootFlag === true) {
|
||
return;
|
||
}
|
||
vm.currentViewMain = 'network-view';
|
||
resetNav(this);
|
||
}
|
||
|
||
document.getElementById("nav-security").onclick = function(){
|
||
if(bootFlag === true) {
|
||
return;
|
||
}
|
||
vm.currentViewMain = 'security-view';
|
||
resetNav(this);
|
||
}
|
||
|
||
// document.getElementById("nav-settings").onclick = function(){
|
||
// if(bootFlag === true) {
|
||
// return;
|
||
// }
|
||
// vm.currentViewMain = 'settings-view';
|
||
// resetNav(this);
|
||
// }
|
||
|
||
function resetNav(that) {
|
||
document.querySelectorAll('.left-nav-button').forEach((el) => {
|
||
el.classList.remove("nav-selected");
|
||
});
|
||
|
||
if (that) {
|
||
that.classList.add('nav-selected');
|
||
}
|
||
}
|
||
|
||
document.getElementById("boot-server-button").onclick = function(){
|
||
if(!loadJson.folders) {
|
||
iziToast.warning({
|
||
title: 'You must set tat least one folder',
|
||
position: 'topCenter',
|
||
timeout: 3500
|
||
});
|
||
resetNav(document.getElementById("nav-directories"));
|
||
vm.currentViewMain = 'folders-view';
|
||
return;
|
||
}
|
||
|
||
if(!loadJson.port) {
|
||
iziToast.warning({
|
||
title: 'You must set the port before booting the server',
|
||
position: 'topCenter',
|
||
timeout: 3500
|
||
});
|
||
resetNav(document.getElementById("nav-network"));
|
||
vm.currentViewMain = 'network-view';
|
||
return;
|
||
}
|
||
|
||
if(bootFlag === true) {
|
||
return;
|
||
}
|
||
|
||
vm.currentViewMain = 'boot-view';
|
||
document.getElementById("boot-server-button").innerHTML = 'Booting...'
|
||
bootFlag = true;
|
||
resetNav();
|
||
|
||
if (document.getElementById("boot-server-checkbox").checked === true) {
|
||
loadJson.autoboot = true;
|
||
}
|
||
|
||
setTimeout(() => {
|
||
ipcRenderer.send('start-server', loadJson);
|
||
setTimeout(() => {
|
||
document.getElementById("boot-server-button").innerHTML = 'Success';
|
||
document.getElementById("boot-server-text").innerHTML = 'Server Booted!';
|
||
|
||
// document.getElementById("post-boot-content").innerHTML = '<div>This window will automatically close in <span id="countdown">30</span> seconds to save on memory</div>';
|
||
const localhostAdd = loadJson.ssl && loadJson.ssl.cert && loadJson.ssl.key ? 'https' : 'http' + '://localhost:' + loadJson.port;
|
||
document.getElementById("post-boot-content").innerHTML = '<div class="row">\
|
||
<div class="col s12">\
|
||
<div class="card">\
|
||
<div class="about-content-section">\
|
||
<div>This window will automatically close in <span id="countdown">30</span> seconds to save on memory</div>\
|
||
<br>\
|
||
<div>Test mStream locally at: <a href="'+localhostAdd+'">'+localhostAdd+'</a></div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>';
|
||
|
||
var countdown = 30;
|
||
setInterval(() => {
|
||
countdown--;
|
||
if(countdown === 0){
|
||
var window = remote.getCurrentWindow();
|
||
window.close();
|
||
}else{
|
||
document.getElementById("countdown").innerHTML = countdown;
|
||
}
|
||
}, 1000);
|
||
}, 2500);
|
||
}, 2500);
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |