mStream/electron/index3.html
2019-10-25 13:59:06 -04:00

1657 lines
80 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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">
<link href="css/boot.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-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-federation" class="left-nav-button waves-effect waves-purple">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 48 48"><path d="M 44.398438 17.300781 C 44.300781 15.898438 43.300781 14.800781 41.898438 14.601563 C 38.398438 14 30.300781 13 24.199219 13 C 18.101563 13 9.601563 13.898438 6 14.5 C 4.699219 14.699219 3.699219 15.800781 3.5 17.199219 L 3 25 C 2.898438 26.601563 4.101563 27.800781 5.699219 28.199219 C 8.601563 29 15.699219 29 24.199219 29 C 32.699219 29 39.5 28.898438 42.300781 28.300781 C 43.898438 28 45.101563 26.699219 45 25.101563 Z M 39 22 L 9 22 C 8.398438 22 8 21.5 8 21 C 8 20.5 8.398438 20 9 20 L 39 20 C 39.601563 20 40 20.5 40 21 C 40 21.5 39.601563 22 39 22 Z" fill="#ffc107"/><path d="M 23.699219 3 C 23.5 3 23.5 3 23.398438 3.199219 C 19 8.398438 15.699219 14.800781 13.398438 22.898438 C 11.800781 28.898438 10.5 36.101563 10 44.699219 C 10 44.898438 10.199219 45 10.300781 45 L 10.5 45 C 10.699219 45 10.699219 45 10.800781 44.800781 C 15.5 38.601563 19.800781 33.699219 26 29.800781 C 27 29.300781 27.5 29 28 29 C 28.199219 29 28.5 29.199219 29 29.800781 C 31.601563 33.398438 34.398438 36.898438 37.199219 41.800781 C 37.199219 42 37.5 42 37.699219 42 C 37.898438 42 38 41.800781 38 41.699219 C 37.300781 34.5 36.199219 27.898438 33.898438 21.398438 C 31.601563 14.699219 28.300781 8.5 24.101563 3.199219 C 24.101563 3 23.898438 3 23.699219 3 Z" fill="#455a64"/><path d="M 32 22.101563 C 30 16.101563 27.199219 10.699219 23.699219 6 C 20 10.800781 17.300781 16.5 15.398438 23.398438 C 14 28.601563 13 33.898438 12.398438 39.601563 C 16.300781 34.800781 20 31.199219 25 28.101563 L 25.101563 28 L 25.199219 28 C 25.398438 27.898438 25.601563 27.800781 25.699219 27.699219 C 26.5 27.300781 27.101563 27 28 27 C 29.398438 27 30.300781 28.199219 30.601563 28.601563 C 31.101563 29.199219 31.5 29.800781 32 30.5 C 33.101563 31.898438 34.101563 33.398438 35.199219 34.898438 C 34.398438 30.199219 33.398438 26 32 22.101563 Z" fill="#c5cae9"/></svg>
<span>Federation</span>
</li>
<li id="nav-network" 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.933v-78.613l-57.766-11.093c-3.883-13.594-9.354-26.549-16.092-38.657l32.701-48.337 l-55.602-55.587l-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.194 C191.568,83.21,178.34,88.654,166,95.592l-48.069-32.61l-55.589,55.562l32.726,48.312c-6.765,12.204-12.277,25.253-16.142,38.959 L21.49,216.841v78.613l57.659,11.078c3.917,13.661,9.426,26.676,16.228,38.834l-32.932,48.67l55.601,55.59l48.83-33.061 c12.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.063 l48.832,33.101l55.588-55.574l-33.072-48.836c6.805-12.221,12.383-25.298,16.279-39.032L490.51,294.933 M255.975,378.017 c-67.441,0-122.115-54.673-122.115-122.114s54.674-122.115,122.115-122.115c67.441,0,122.115,54.673,122.115,122.115 S323.416,378.017,255.975,378.017" fill="#F56F6C"/><path d="M254.297,378v0.006c0.439,0.006,0.877,0.01,1.316,0.01C255.322,378.014,255.289,378.005,254.297,378 M255.676,133.789c-0.461,0-0.92,0.004-1.379,0.011v0.005C255.313,133.801,255.359,133.791,255.676,133.789 M255.975,133.788 l-0.086,0.001c67.402,0.047,122.113,54.702,122.113,122.114c0,67.405-54.744,122.055-122.135,122.114h0.107 c67.441,0,122.115-54.673,122.115-122.114S323.416,133.788,255.975,133.788 M393.775,62.645l-0.014,0.009l55.592,55.579 l-32.701,48.337c6.738,12.108,12.209,25.063,16.092,38.657l57.766,11.093v78.613v-78.613l-57.766-11.093 c-3.883-13.594-9.268-26.549-16.006-38.657l32.66-48.337L393.775,62.645 M295.127,22.058h-40.83H295.127l11.043,57.14 c13.846,3.933,27.029,9.225,39.338,16.109l0.006-0.003c-12.307-6.884-25.494-12.174-39.338-16.106L295.127,22.058" fill="#F2F2F2"/><path d="M295.127,22.058h-40.83V133.8c0.459-0.007,0.918-0.011,1.379-0.011l0.125-0.001l0.088,0.001l0.086-0.001 c67.441,0,122.115,54.673,122.115,122.115s-54.674,122.114-122.115,122.114h-0.107h-0.109l-0.145-0.001 c-0.439,0-0.877-0.004-1.316-0.01v111.936h40.92l11.096-57.531c13.684-3.894,26.717-9.275,38.896-16.063l48.832,33.101 l55.588-55.574l-33.072-48.836c6.805-12.221,12.383-25.298,16.279-39.032l57.674-11.073v-78.613l-57.766-11.093 c-3.883-13.594-9.354-26.549-16.092-38.657l32.701-48.337l-55.592-55.579l-48.244,32.654l-0.004-0.003l-0.006,0.003 c-12.309-6.885-25.492-12.177-39.338-16.109L295.127,22.058" fill="#E96966"/><path d="M256.072,111.928c-79.505,0-143.957,64.451-143.957,143.956c0,79.503,64.452,143.955,143.957,143.955 c79.504,0,143.955-64.452,143.955-143.955C400.027,176.379,335.576,111.928,256.072,111.928z M256.072,329.569 c-40.696,0-73.687-32.99-73.687-73.686c0-40.697,32.991-73.687,73.687-73.687c40.695,0,73.686,32.989,73.686,73.687 C329.758,296.579,296.768,329.569,256.072,329.569z" fill="#FFF"/></svg>
<span>Advanced</span>
</li>
</ul>
<div class="left-nav-menu-header">
Other
</div>
<ul class="left-nav-menu">
<li id="nav-auto-dns" class="left-nav-button waves-effect waves-light">
<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>mStream RPN</span>
</li>
<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 axios = require('axios');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto')
const Login = require('../modules/login');
const shell = require('electron').shell
const apiEndpoint = 'https://api.mstream.io';
const configFile = path.join(app.getPath('userData'), 'save/server-config-v2.json');
var loadJson = {};
var editThisUser;
var bootFlag = false;
// 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;
}
if (!loadJson.scanOptions) {
loadJson.scanOptions = {};
}
if (!loadJson.storage) {
loadJson.storage = {};
}
if (!loadJson.federation) {
loadJson.federation = {};
}
if (!loadJson.transcode) {
loadJson.transcode = {};
}
if (!loadJson.secret) {
loadJson.secret = crypto.randomBytes(Math.ceil(128/2)).toString('base64').slice(0,128);
}
if (typeof loadJson.noUpload !== 'boolean') {
loadJson.noUpload = false;
}
if (!loadJson.storage.albumArtDirectory) {
loadJson.storage.albumArtDirectory = path.join(app.getPath('userData'), 'image-cache');
}
if (!loadJson.storage.dbDirectory) {
loadJson.storage.dbDirectory = path.join(app.getPath('userData'), 'save');
}
if (!loadJson.storage.logsDirectory) {
loadJson.storage.logsDirectory = path.join(app.getPath('userData'), 'logs');
}
if (!loadJson.storage.syncConfigDirectory) {
loadJson.storage.syncConfigDirectory = path.join(app.getPath('userData'), 'sync');
}
if (!loadJson.transcode.ffmpegDirectory) {
loadJson.transcode.ffmpegDirectory = path.join(app.getPath('userData'), 'ffmpeg')
}
if (!loadJson.transcode.enabled) {
loadJson.transcode.enabled = false;
}
if (!loadJson.transcode.defaultCodec) {
loadJson.transcode.defaultCodec = 'opus';
}
if (!loadJson.transcode.defaultBitrate) {
loadJson.transcode.defaultBitrate = '96k';
}
// Auto DNS Testing
if (!loadJson.ddns) {
loadJson.ddns = {};
} else {
loadJson.ddns.tested = false;
testAutoDns();
}
async function testAutoDns() {
try {
const loginRes = await axios({
method: 'post',
url: apiEndpoint + '/login',
headers: { 'accept': 'application/json' },
responseType: 'json',
data: {
email: loadJson.ddns.email,
password: loadJson.ddns.password
}
});
loadJson.ddns.tested = true;
} catch(err) {}
}
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.length === 0 ){
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.length === 0 ){
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>',
});
var userAccordionHolder;
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>{{displayName(value)}}{{isGuest(value)}} </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">\
<a v-if="value\[\'lastfm-user\'\]" v-on:click="removeLastFmUser(value, key, index)" href="#!">Remove last.fm account</a>\
<a v-else v-on:click="openLastFmModal(value, key, index)" href="#!">Add last.fm account</a>\
<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 () {
userAccordionHolder = this;
this.instances = M.Collapsible.init(document.querySelectorAll('.collapsible-folders'), { onCloseEnd: this.closeOverride });
},
beforeDestroy: function() {
userAccordionHolder = null;
this.instances[0].destroy();
},
methods: {
displayName: function(arr) {
if(arr['lastfm-user']) {
return ' (last.fm: ' + arr['lastfm-user'] + ')';
}
return '';
},
isGuest: function(arr) {
if(arr.guest === true) {
return ' (guest)';
}
return '';
},
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');
}],
]
});
},
openLastFmModal: function(value, key, index) {
editThisUser = key;
if (vModal.$children[0]) {
vModal.componentKey = !vModal.componentKey;
}
vModal.currentViewModal = 'user-lastfm-view';
modalInstance[0].open();
},
removeLastFmUser: function(value, key, index) {
iziToast.question({
timeout: 20000,
close: false,
overlayClose: true,
overlay: true,
displayMode: 'once',
id: 'question',
zindex: 99999,
title: "Remove last.fm login for <b>'" + key + "'</b>?",
position: 'center',
buttons: [
['<button><b>Delete</b></button>', (instance, toast) => {
loadJson.users[key]['lastfm-user'] = undefined;
loadJson.users[key]['lastfm-password'] = undefined;
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
// this.instances[0].close(index);
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
this.$forceUpdate()
}, 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 pad-15">\
<!-- <a v-on:click="openLastFmModal()" href="#!">Add last.fm account</a> -->\
<label class="input-field">\
<input id="is-new-user-guest" type="checkbox"/>\
<span>Guest Account</span>\
</label>\
</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: {
openLastFmModal: function() {
editThisUser = null;
if (vModal.$children[0]) {
vModal.componentKey = !vModal.componentKey;
}
vModal.currentViewModal = 'user-lastfm-view';
modalInstance[0].open();
},
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
};
if (document.getElementById("is-new-user-guest").checked === true) {
loadJson.users[this.newUsername].guest = true;
}
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 federationView = Vue.component('federation-view', {
data() {
return {
lJson: loadJson,
};
},
template: '\
<div>\
<div class="row">\
<div class="col s12">\
<div class="card">\
<div class="section-header">Federation</div>\
<div class="row row-mod">\
<div class="col s12">\
Federation allows you to sync files between mStream servers. To sync your files to another server, you can create a Invite Token through the Web UI\
</div>\
</div>\
</div>\
<div class="card">\
<div class="row row-mod">\
<div class="section-header">Select Federation Folder</div>\
</div>\
<div class="row row-mod">\
<div class="col s12">\
Federated directories will be downloaded here\
</div>\
<div class="col s12 port-form-container">\
<div class="input-field">\
<input v-on:click="selectSyncPath($event)" v-model="lJson.federation.folder" @blur="updateConfig()" type="text" placeholder="Click to select directory" required />\
<span class="helper-text">Federation is disabled until this value is set</span>\
</div>\
</div>\
</div>\
<div class="row row-mod clearfix">\
<a class="reset-defaults-link" href="#!" v-on:click="resetStorageValues">Clear (Disables Federation)</a>\
</div>\
</div>\
<div class="card">\
<div>\
<div class="section-header">Config Directory</div>\
</div>\
<div class="row row-mod">\
<div class="col s12">\
Your federation configuration files and security keys are stored here. If you change this directory, you will generate new keys and config files.\
</div>\
<div class="col s12 port-form-container">\
<div class="input-field">\
<input v-on:click="selectSyncConfigPath($event)" v-model="lJson.storage.syncConfigDirectory" @blur="updateConfig()" type="text" placeholder="Click to select directory" required />\
</div>\
</div>\
</div>\
<div class="row row-mod clearfix">\
<a class="reset-defaults-link" href="#!" v-on:click="resetValue" >Reset To Default Values</a>\
</div>\
</div>\
</div>\
</div>\
</div>',
methods: {
updateConfig: function() {
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
},
resetStorageValues: function(e) {
loadJson.federation.folder = '';
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
},
selectSyncPath: function(el) {
dialog.showOpenDialog({properties: ['openDirectory']}, (selectedFile) => {
if(selectedFile.length === 0){
return;
}
el.target.value = selectedFile[0];
loadJson.federation.folder = selectedFile[0];
el.target.blur();
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
});
},
selectSyncConfigPath: function(el) {
dialog.showOpenDialog({properties: ['openDirectory']}, (selectedFile) => {
if (selectedFile.length === 0){
return;
}
el.target.value = selectedFile[0];
loadJson.storage.syncConfigDirectory = selectedFile[0];
el.target.blur();
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
});
},
resetValue: function(e) {
loadJson.storage.syncConfigDirectory = path.join(app.getPath('userData'), 'sync');
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
}
}
});
const networkView = Vue.component('network-view', {
data() {
return {
lJson: loadJson,
selectInstance: null,
specialTranscodeValue: `${loadJson.transcode.defaultCodec}|${loadJson.transcode.defaultBitrate}`
};
},
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>\
<div class="row row-mod">\
<div class="col m6 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 class="col s6">\
</div>\
</div>\
</div>\
<div class="card">\
<div class="row row-mod">\
<div class="section-header">Transcoding</div>\
</div>\
<div class="row row-mod">\
<div class="col s12">\
Transcoding allows you to listen to your music in a different format. It\'s typically used for low bandwidth streaming on mobile\
</div>\
<div class="col s12 transcoding-checkbox">\
<label class="input-field">\
<input v-model="lJson.transcode.enabled" v-on:blur="updateConfig" id="allow-transcode-checkbox" type="checkbox" checked="checked" />\
<span>Enable Transcoding</span>\
</label>\
</div>\
<div class="input-field col s12">\
<select v-model="specialTranscodeValue" @change="onTranscodeSelectChange($event)" id="transcode-defaults">\
<option value="mp3|128k">mp3 - 128kbps</option>\
<option value="mp3|192k">mp3 - 192kbps</option>\
<option value="opus|64k">opus - 64kbps</option>\
<option value="opus|96k">opus - 96kbps</option>\
<option value="opus|128k">opus - 128kbps</option>\
<option value="aac|64k">aac - 64kbps</option>\
<option value="aac|96k">aac - 96kbps</option>\
<option value="aac|128k">aac - 128kbps</option>\
</select>\
<label for="transcode-defaults">Select Default Bitrate</label>\
</div>\
</div>\
</div>\
<div class="card">\
<div class="row row-mod">\
<div class="section-header">File Uploading</div>\
</div>\
<div class="row row-mod">\
<div class="col s12">\
You can upload files in the Web App by dropping them into the file explorer\
</div>\
<div class="col s12 port-form-container">\
<label class="input-field">\
<input v-model="lJson.noUpload" v-on:blur="updateConfig" id="allow-upload-checkbox" type="checkbox" checked="checked" />\
<span>Disable File Uploading</span>\
</label>\
</div>\
</div>\
</div>\
<div class="card">\
<div class="row row-mod">\
<div class="section-header">Storage</div>\
</div>\
<div class="row row-mod">\
<div class="col s12 port-form-container">\
<div class="input-field">\
<input v-on:click="selectDbPath($event)" v-model="lJson.storage.dbDirectory" @blur="updateConfig()" id="server-db-folder" type="text" class="active" required />\
<label class="active" for="server-db-folder">Database Folder</label>\
</div>\
</div>\
<div class="col s12 port-form-container">\
<div class="input-field">\
<input v-on:click="selectArtPath($event)" v-model="lJson.storage.albumArtDirectory" @blur="updateConfig()" id="server-aa-folder" type="text" required />\
<label class="active" for="server-aa-folder">Album Art Folder</label>\
<span class="helper-text">Album art stored here</span>\
</div>\
</div>\
<div class="col s12 port-form-container">\
<div class="input-field">\
<input v-on:click="selectLogsPath($event)" v-model="lJson.storage.logsDirectory" @blur="updateConfig()" id="server-logs-folder" type="text" class="active" required />\
<label class="active" for="server-logs-folder">Logs Folder</label>\
<span class="helper-text">No logs written if this is empty</span>\
</div>\
</div>\
<div class="col s12 port-form-container">\
<div class="input-field">\
<input v-on:click="selectFfmpegPath($event)" v-model="lJson.transcode.ffmpegDirectory" @blur="updateConfig()" id="server-ffmpeg-folder" type="text" class="active" required />\
<label class="active" for="server-ffmpeg-folder">FFmpeg Folder</label>\
<span class="helper-text">FFmpeg will be downloaded to this directory</span>\
</div>\
</div>\
</div>\
<div class="row row-mod clearfix">\
<a class="reset-defaults-link" href="#!" v-on:click="resetStorageValues" >Reset To Default Values</a>\
</div>\
</div>\
</div>\
</div>\
</div>',
mounted: function () {
this.selectInstance = M.FormSelect.init(document.querySelectorAll("#transcode-defaults"));
},
beforeDestroy: function() {
this.selectInstance[0].destroy();
},
methods: {
updateConfig: function() {
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
},
onTranscodeSelectChange: function(event) {
const vals = event.target.value.split('|');
loadJson.transcode.defaultBitrate = vals[1];
loadJson.transcode.defaultCodec = vals[0];
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
},
resetStorageValues: function() {
loadJson.storage.albumArtDirectory = path.join(app.getPath('userData'), 'image-cache');
loadJson.storage.dbDirectory = path.join(app.getPath('userData'), 'save');
loadJson.storage.logsDirectory = path.join(app.getPath('userData'), 'logs');
loadJson.transcode.ffmpegDirectory = path.join(app.getPath('userData'), 'ffmpeg');
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
},
selectDbPath: function(el) {
dialog.showOpenDialog({properties: [ 'openDirectory']}, (selectedFile) => {
if(selectedFile.length === 0){
return;
}
el.target.value = selectedFile[0];
loadJson.storage.dbDirectory = selectedFile[0];
el.target.blur();
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
});
},
selectFfmpegPath: function(el) {
dialog.showOpenDialog({ properties: ['openDirectory'] }, (selectedDirectory) => {
if (selectedDirectory.length === 0){
return;
}
el.target.value = selectedDirectory[0];
loadJson.transcode.ffmpegDirectory = selectedDirectory[0];
el.target.blur();
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
});
},
selectLogsPath: function(el) {
dialog.showOpenDialog({properties: [ 'openDirectory']}, (selectedFile) => {
if(selectedFile.length === 0 ){
return;
}
el.target.value = selectedFile[0];
loadJson.storage.logsDirectory = selectedFile[0];
el.target.blur();
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
});
},
selectArtPath: function(el) {
dialog.showOpenDialog({properties: [ 'openDirectory']}, (selectedFile) => {
if(selectedFile.length === 0){
return;
}
el.target.value = selectedFile[0];
loadJson.storage.albumArtDirectory = selectedFile[0];
el.target.blur();
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
});
},
}
});
const securityView = Vue.component('security-view', {
data: function() {
return {
currentViewSsl: (loadJson.ddns.tested) ? ('auto-dns-logged-in-view') : ((!loadJson.ssl.cert && !loadJson.ssl.key) ? 'security-view-signup' : 'security-view-diy')
};
},
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">\
This feature is not enabled by default. You must provide your own SSL certificates to enable encryption. SSL certificates can be registered for free with <a href="https://certbot.eff.org/">Certbot</a>\
<br><br>\
mStream RPN takes care of registering certificates and configuring server encryption. <a href="https://mstream.io/reverse-proxy-network">Read more about it here</a>\
</div>\
</div>\
</div>\
</div>\
</div>\
<div class="row">\
<div class="col s12 security-container">\
<transition name="component-fade" mode="out-in">\
<component v-bind:is="currentViewSsl">\
</component>\
</transition>\
</div>\
</div>\
</div>',
});
const securityViewDiyKeys = Vue.component('security-view-diy', {
data: function() {
return {
ssl: loadJson.ssl
};
},
template: '\
<div class="card security-diy">\
<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 File</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>',
mounted: function() {
M.updateTextFields();
},
methods: {
clearSSL: function() {
loadJson.ssl.key = '';
loadJson.ssl.cert = '';
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
M.updateTextFields();
this.$parent._data.currentViewSsl = 'security-view-signup';
},
selectKeyFile: function(el) {
dialog.showOpenDialog({properties: [ 'openFile']}, (selectedFile) => {
if(selectedFile.length === 0){
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.length === 0){
return;
}
el.target.value = selectedFile;
loadJson.ssl.cert = selectedFile;
el.target.blur();
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
});
},
}
});
const securityViewSignup = Vue.component('security-view-signup', {
data: function() {
return {
ssl: loadJson.ssl
};
},
template: '\
<div class="ssl-choice card no-pad-bot">\
<div v-on:click="goToAutoDns()" class="signup-button waves-effect waves-default">\
<p class="center bigger-text"><b>mStream RPN</b></p>\
<svg height="88" width="100%" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><path d="M46.698 33.004H31.99c-3.306 0-5.986 2.68-5.986 5.986v8.295c0 3.637 2.948 6.585 6.585 6.585H46.1c3.637 0 6.585-2.948 6.585-6.585V38.99C52.684 35.684 50.004 33.004 46.698 33.004zM49.547 47.588c0 1.93-1.564 3.494-3.494 3.494H32.635c-1.93 0-3.494-1.564-3.494-3.494v-8.302c0-1.93 1.564-3.494 3.494-3.494h13.419c1.93 0 3.494 1.564 3.494 3.494V47.588zM31.99 31.935h2.01V28.42c0-2.947 2.398-5.345 5.345-5.345 2.947 0 5.345 2.398 5.345 5.345v3.515h2.01c.574 0 1.133.071 1.668.201V28.42c0-4.975-4.047-9.022-9.022-9.022s-9.022 4.047-9.022 9.022v3.716C30.857 32.006 31.415 31.935 31.99 31.935z" fill="#3E4753"/><path d="M45.095,37.451H33.593c-1.287,0-2.331,1.043-2.331,2.331v7.311c0,1.287,1.043,2.331,2.331,2.331h11.503 c1.287,0,2.331-1.043,2.331-2.331v-7.311C47.426,38.494,46.382,37.451,45.095,37.451z M40.705,43.86v2.064 c0,0.748-0.612,1.361-1.361,1.361c-0.748,0-1.361-0.612-1.361-1.361V43.86c-0.599-0.426-0.991-1.126-0.991-1.918 c0-1.299,1.053-2.353,2.353-2.353c1.299,0,2.353,1.054,2.353,2.353C41.697,42.734,41.305,43.434,40.705,43.86z" fill="#2E7EB8"/><g><path d="M60.7,29.153c-1.119-1.406-2.581-2.503-4.224-3.19c-0.306-4.111-2.037-7.917-4.952-10.832 c-3.225-3.225-7.542-5.002-12.154-5.002c-5.136,0-9.986,2.323-13.291,6.292c-1.267-0.358-2.555-0.539-3.848-0.539 c-6.201,0-11.544,3.92-13.466,9.701C6.783,26.166,5,27.308,3.638,28.889C1.937,30.865,1,33.402,1,36.035 c0,5.957,4.846,10.803,10.803,10.803h2.839h8.154V42.25h-8.154h-2.839c-3.427,0-6.215-2.788-6.215-6.215 c0-3.173,2.246-5.796,5.341-6.238l1.571-0.224l0.344-1.549c0.988-4.446,4.848-7.552,9.386-7.552c1.27,0,2.546,0.263,3.792,0.782 l1.731,0.721l1.051-1.554c2.416-3.571,6.365-5.703,10.564-5.703c7.047,0,12.567,5.52,12.567,12.567v2.193l1.789,0.404 c2.842,0.642,4.768,3.145,4.684,6.086l-0.001,0.066c0,2.041-0.993,3.85-2.517,4.984v5.154c4.128-1.511,7.091-5.462,7.105-10.105 C63.061,33.55,62.246,31.095,60.7,29.153z" fill="#2E7EB8"/></g></svg>\
<p class="signup-price">Sign Up / Login</p>\
</div>\
<div v-on:click="goToDiy()" class="diy-button clearfix waves-effect waves-default">\
<svg class="ssl-svg3 center" height="60" width="100%" viewBox="0 0 33 33" xmlns="http://www.w3.org/2000/svg"><path d="M13.641 8.239c2.021-1.023 3.667-2.107 4.591-2.76 1.732 1.223 5.988 3.967 10.535 4.972-.121 1.343-.432 3.75-1.211 6.384l2.445 1.14c1.351-4.386 1.468-8.24 1.476-8.577l.026-1.163-1.152-.17C24.813 7.249 19.1 2.848 19.043 2.805l-.81-.631-.81.63C17.387 2.833 15 4.67 11.825 6.192 12.553 6.762 13.167 7.456 13.641 8.239zM18.232 28.08c-4.332-1.375-6.896-4.98-8.415-8.664-.851.328-1.754.504-2.677.514 1.758 4.531 4.947 9.158 10.734 10.795l.357.102.359-.102c3.113-.881 5.475-2.629 7.267-4.76l-2.523-1.178C21.988 26.24 20.316 27.418 18.232 28.08z" fill="#2B79C2"/><path d="M26.357 16.275c.527-1.839.818-3.558.979-4.827-3.756-1.059-7.178-3.089-9.104-4.373-.992.662-2.383 1.521-4.011 2.344.172.443.307.904.396 1.382L26.357 16.275zM18.232 26.688c1.516-.551 2.782-1.422 3.845-2.486l-11.01-5.135C12.42 22.287 14.627 25.379 18.232 26.688z" fill="#2B79C2"/><path d="M30.555,19.479l-16.966-7.912c-0.23-2.219-1.58-4.266-3.749-5.276C7.375,5.141,4.561,5.655,2.662,7.37 L7.616,9.68l0.639,3.115l-2.797,1.512l-4.953-2.311c-0.094,2.557,1.322,5.042,3.787,6.193c2.168,1.01,4.604,0.729,6.451-0.52 l16.966,7.91c1.686,0.787,3.688,0.057,4.475-1.627C32.969,22.268,32.24,20.264,30.555,19.479z M28.001,23.615 c-0.731-0.34-1.048-1.211-0.707-1.943s1.212-1.049,1.943-0.707s1.049,1.211,0.708,1.943C29.604,23.641,28.733,23.957,28.001,23.615 z" fill="#454C51"/></svg>\
<div><p class="center bigger-text"><b>DIY</b></p></div>\
</div>\
</div>',
methods: {
goToDiy: function() {
this.$parent._data.currentViewSsl = 'security-view-diy';
},
goToAutoDns: function() {
if(bootFlag === true) {
return;
}
vm.currentViewMain = 'auto-dns-view';
resetNav(document.getElementById("nav-auto-dns"));
}
}
});
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>Edit User</h4>\
<p>User: <b>{{currentUser}}</b></p>\
<label for="edit-user-dirs">Select User\'s Directories</label>\
<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 class="edit-user-guest">\
<label class="input-field">\
<input v-model="users[currentUser].guest" v-on:blur="updateConfig" type="checkbox"/>\
<span>Guest Account</span>\
</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\
</button>\
</div>\
</form>',
mounted: function () {
this.selectInstance = M.FormSelect.init(document.querySelectorAll("#edit-user-dirs"));
},
beforeDestroy: function() {
this.selectInstance[0].destroy();
},
methods: {
updateConfig: function() {
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
},
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 userLastFmView = Vue.component('user-lastfm-view', {
data() {
return {
currentUser: editThisUser,
users: loadJson.users,
lastFmUser: '',
lastFmPassword: '',
lastFmVerified: false,
pending: false
};
},
template: '\
<form @submit.prevent="addLastFmAccount" id="add-lastfm-user-form">\
<div class="modal-content">\
<h4>Add last.fm Account</h4>\
<p v-if="currentUser">User: <b>{{currentUser}}</b></p>\
<div class="input-field directory-name-field">\
<input @blur="maybeResetForm()" v-model="lastFmUser" id="lastfm-username" required type="text" class="validate">\
<label for="lastfm-username">last.fm user</label>\
</div>\
<div class="input-field directory-name-field">\
<input @blur="maybeResetForm()" v-model="lastFmPassword" id="lastfm-password" required type="password" class="validate">\
<label for="lastfm-password">last.fm password</label>\
</div>\
</div>\
<div class="modal-footer">\
<a href="#!" class="modal-close waves-effect waves-green btn-flat">Go Back</a>\
<button :disabled="pending" id="submit-reset-password-form" class="btn green waves-effect waves-light" type="submit">\
{{pending === false ? "LOGIN": "Pending..."}}\
</button>\
</div>\
</form>',
methods: {
addLastFmAccount: async function() {
this.pending = true;
// validate last.fm credentials
if(await testLastFm(this.lastFmUser, this.lastFmPassword) === false) {
this.pending = false;
iziToast.warning({
title: 'Failed to Login to Last.fm',
position: 'topCenter',
timeout: 3500
});
return;
}
this.pending = false;
if (this.currentUser) {
loadJson.users[this.currentUser]['lastfm-user'] = this.lastFmUser;
loadJson.users[this.currentUser]['lastfm-password'] = this.lastFmPassword;
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
modalInstance[0].close();
// Force update component if it exists
if(userAccordionHolder) {
userAccordionHolder.$forceUpdate();
}
return;
}
},
maybeResetForm: function() {
if (this.lastFmUser === '' && this.lastFmPassword === '') {
document.getElementById("add-lastfm-user-form").reset();
}
},
}
});
async function testLastFm(username, password) {
const apiKey1 = '25627de528b6603d6471cd331ac819e0';
const apiKey2 = 'a9df934fc504174d4cb68853d9feb143';
const token = crypto.createHash('md5').update(username + crypto.createHash('md5').update(password, 'utf8').digest("hex"), 'utf8').digest("hex");
const cryptoString = `api_key${apiKey1}authToken${token}methodauth.getMobileSessionusername${username}${apiKey2}`;
const hash = crypto.createHash('md5').update(cryptoString, 'utf8').digest("hex");
// Try logging in
try {
await axios({
method: 'get',
url: `http://ws.audioscrobbler.com/2.0/?method=auth.getMobileSession&username=${username}&authToken=${token}&api_key=${apiKey1}&api_sig=${hash}`
});
}catch (err) {
return false;
}
return true;
}
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>",
});
const autoDnsView = Vue.component('auto-dns-view', {
data() {
return {
pending: false,
currentViewDdnsLogin: 'ddns-view-login',
username: '',
password: ''
};
},
template: '\
<div>\
<div class="row auto-dns-selection">\
<div class="col m6 s12 auto-dns-selection-item">\
<div v-on:click="goToSignUp()" class="card auto-dns-card auto-dns-sign-up-item">\
<div class="section-header">Sign Up</div>\
<svg viewBox="0 0 128 128" height="120" width="100%" xmlns="http://www.w3.org/2000/svg"><path d="M94.894,68.493c-1.379-4.803-5.663-8.043-10.661-8.063h-0.631c0.146-3.979-0.709-7.176-2.677-10.032 c-2.403-3.498-5.605-5.677-9.788-6.663c-1.014-0.237-2.09-0.357-3.197-0.357c-4.143,0-9.78,1.83-13.333,6.942 c-1.344-0.545-2.772-0.822-4.249-0.822c-5.846,0-10.765,4.5-11.299,10.287c-0.633,0.333-1.228,0.719-1.773,1.149 c-1.67,1.311-2.967,3.049-3.752,5.025c-1.284,3.234-1.095,6.848,0.518,9.914c1.72,3.263,4.408,5.412,7.774,6.215l0.023-0.096 c0.691,0.135,2.776,0.148,9.075,0.188l5.487,0.037l0.012,0.071l0.348,0.003c3.083,0.029,9.591,0.055,15.166,0.076l4.354,0.018 l-0.024,0.042l6.333,0.045c0.85,0,1.507-0.01,2.126-0.029c3.362-0.11,6.442-1.754,8.451-4.513 C95.189,75.181,95.816,71.743,94.894,68.493z M43.606,62.33c-0.108-0.479-0.162-0.98-0.162-1.494c0-3.807,3.102-6.904,6.915-6.904 c1.844,0,3.579,0.724,4.884,2.039l0.184,0.186l0.247-0.087c0.697-0.244,1.267-0.758,1.565-1.407 c0.272-0.583,0.566-1.105,0.901-1.598c2.562-3.852,6.742-5.224,9.8-5.224c0.748,0,1.483,0.08,2.181,0.239 c3.054,0.717,5.386,2.302,7.132,4.846c1.572,2.293,2.148,5.027,1.813,8.601l-0.039,0.401c-0.078,0.745,0.174,1.503,0.693,2.082 c0.514,0.561,1.241,0.884,1.998,0.884h2.496c3.005,0.01,5.571,1.949,6.385,4.831c0.55,1.897,0.177,3.933-1.022,5.587 c-1.191,1.632-3.01,2.611-4.991,2.682c-0.573,0.019-1.188-0.026-1.983,0.019H45.094c-3.293-0.01-5.615-1.387-7.097-4.216 c-1.002-1.913-1.118-4.172-0.32-6.194c0.504-1.246,1.32-2.342,2.346-3.154l0.12-0.089c0.522-0.389,1.062-0.703,1.594-0.927 c0.036-0.013,0.109-0.031,0.261-0.073c0.359-0.102,0.902-0.256,1.423-0.587l0.247-0.156L43.606,62.33z" fill="#2E79BE"/><path d="M116.277,93.951c0.098-0.469,0.137-0.958,0.137-1.447v-59.98c0-5.357-4.047-9.677-9.032-9.677h-1.916 h-4.086H20.638c-4.985,0-9.032,4.321-9.032,9.677v59.98c0,0.489,0.039,0.977,0.098,1.447H2.535v1.505 c0,5.376,4.027,9.697,9.012,9.697h104.906c4.966,0,9.013-4.321,9.013-9.697v-1.505H116.277z M71.194,100.246 c0,1.036-0.684,1.877-1.564,1.877H58.37c-0.88,0-1.564-0.821-1.564-1.877v-1.212c0-1.056,0.684-1.896,1.564-1.896H69.63 c0.88,0,1.564,0.86,1.564,1.896V100.246z M107.577,93.951H20.423V32.368h79.093h3.882h4.179V93.951z" fill="#2D3E4F"/></svg>\
</div>\
</div>\
<div class="col m6 s12 auto-dns-selection-item">\
<div class="card auto-dns-card">\
<div class="section-header">Login</div>\
<form id="login-form" class="" @submit.prevent="login">\
<div class="row row-mod">\
<div class="input-field directory-name-field col s12">\
<input @blur="maybeResetForm()" v-model="username" id="login-username" required type="text" class="validate">\
<label for="login-username">Username</label>\
</div>\
<div class="input-field directory-name-field col s12">\
<input @blur="maybeResetForm()" v-model="password" id="login-password" required type="password" class="validate">\
<label for="login-password">Password</label>\
</div>\
<div class="col s12">\
<button :disabled="pending" class="btn green waves-effect waves-light login-button" type="submit">\
{{loginButtonText}}\
</button>\
</div>\
</div>\
</form>\
</div>\
</div>\
</div>\
<div class="row">\
<div class="col s12">\
<div class="card">\
<div class="row row-mod">\
<div class="section-header">mStream RPN Service</div>\
<div class="col s12">\
<ul class="browser-default">\
<li>Get your own personal domain @ https://your-name.mstream.io</li>\
<li>Automatically configures SSL Encryption for your server</li>\
<li>State of the art \'Hole Punching\' software guarantees your server stays online as long as you have a working internet connection</li>\
<li>IP Obfuscation hides your IP address and adds an additional layer of security</li>\
</ul>\
mStream RPN Service comes with everything you need to get your new server online in seconds. Think of it as your private cloud.\
</div>\
</div>\
</div>\
</div>\
</div>\
</div>',
computed: {
loginButtonText: function() {
if(this.pending) {
return 'pending...';
}
return 'Login';
}
},
methods: {
goToSignUp: function() {
shell.openExternal('https://mstream.io/reverse-proxy-network');
},
maybeResetForm: function() {
if (this.username === '' && this.password === '') {
document.getElementById("login-form").reset();
}
},
login: async function() {
try {
this.pending = true;
const loginRes = await axios({
method: 'post',
url: apiEndpoint + '/login',
headers: { 'accept': 'application/json' },
responseType: 'json',
data: {
email: this.username,
password: this.password
}
});
const configRes = await axios({
method: 'get',
url: apiEndpoint + '/account/info',
headers: { 'x-access-token': loginRes.data.token, 'accept': 'application/json' },
responseType: 'json'
});
loadJson.ddns = {
email: this.username,
password: this.password,
token: loginRes.data.token,
tested: true,
url: configRes.data.subdomain + '.' + configRes.data.domain
};
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
iziToast.success({
title: 'Logged In',
position: 'topCenter',
timeout: 3500
});
vm.currentViewMain = 'auto-dns-logged-in-view';
this.pending = false;
} catch(err) {
this.pending = false;
iziToast.warning({
title: 'Failed to Login',
position: 'topCenter',
timeout: 3500
});
}
}
}
});
const autoDnsLoggedInView = Vue.component('auto-dns-logged-in-view', {
template: '\
<div class="row">\
<div class="col s12">\
<div class="card">\
<div class="row row-mod">\
<div class="section-header">mStream RPN</div>\
<div class="col s12">\
<div>You are logged in as: <b>{{loadJson.ddns.email}}</b></div>\
<div>Your Public URL: <b>https://{{loadJson.ddns.url}}</b></div>\
<br>\
<div>SSL Encryption: <b>Enabled</b></div>\
<br>\
<div v-if="this.$parent && this.$parent.$el && this.$parent.$el.id === \'switcherMain\'"><a v-on:click="logout()" href="javascript:;">Logout</a></div>\
</div>\
</div>\
</div>\
</div>\
</div>',
methods: {
logout: function() {
loadJson.ddns = {};
vm.currentViewMain = 'auto-dns-view';
},
}
});
var vModal = new Vue({
el: '#switcherModal',
components: {
'user-password-view': userPasswordView,
'user-folders-view': userFoldersView,
'user-lastfm-view': userLastFmView
},
data: {
componentKey: false,
currentViewModal: false
}
});
var vm = new Vue({
el: '#switcherMain',
components: {
'folders-view': foldersView,
'about-view': aboutView,
'users-view': usersView,
'network-view': networkView,
'federation-view': federationView,
'security-view': securityView,
'boot-view': bootView,
'auto-dns-view': autoDnsView
},
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-auto-dns").onclick = function(){
if(bootFlag === true) {
return;
}
vm.currentViewMain = (loadJson.ddns.tested) ? 'auto-dns-logged-in-view' : 'auto-dns-view';
resetNav(this);
}
document.getElementById("nav-federation").onclick = function(){
if(bootFlag === true) {
return;
}
vm.currentViewMain = 'federation-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>