mirror of
https://github.com/IrosTheBeggar/mStream.git
synced 2025-10-27 07:31:02 +00:00
admin panel
This commit is contained in:
parent
03c13848c9
commit
95c6c7aeac
@ -47,7 +47,7 @@
|
||||
<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>
|
||||
<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">
|
||||
|
||||
@ -114,6 +114,8 @@ exports.serveIt = config => {
|
||||
});
|
||||
}
|
||||
|
||||
require('./src/api/admin.js').setup(mstream, program);
|
||||
|
||||
// Album art endpoint
|
||||
mstream.use('/album-art', express.static(program.storage.albumArtDirectory));
|
||||
// Download Files API
|
||||
|
||||
@ -28,18 +28,21 @@
|
||||
<link href="/public/fonts/jura.css" rel="stylesheet">
|
||||
|
||||
<script src="/public/js/lib/vue.min.js"></script>
|
||||
<script src="/public/js/lib/axios.js"></script>
|
||||
<script defer src="/public/js/lib/materialize.js"></script>
|
||||
<script defer src="/public/js/spa.js"></script>
|
||||
<script defer src="/public/js/api.js"></script>
|
||||
<script defer src="/public/js/admin.js"></script>
|
||||
|
||||
<!-- iziToast -->
|
||||
<script src="/public/js/lib/izi-toast.min.js"></script>
|
||||
<link rel="stylesheet" href="/public/css/izi-toast.min.css">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="/public/css/spinner.css">
|
||||
<link rel="stylesheet" href="/public/css/materialize.css">
|
||||
<link rel="stylesheet" href="/public/css/spa.css">
|
||||
<link rel="stylesheet" href="/public/css/spinner.css">
|
||||
<link rel="stylesheet" href="/public/css/admin.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@ -98,6 +101,17 @@
|
||||
<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>
|
||||
</div>
|
||||
<div class="side-nav-item waves-effect waves-purple" onclick="changeView('notifications-view', this)">
|
||||
<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>
|
||||
</div>
|
||||
<div class="side-nav-header">
|
||||
<span>Security</span>
|
||||
</div>
|
||||
<div class="side-nav-item waves-effect waves-purple" onclick="changeView('notifications-view', this)">
|
||||
<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>Lock Admin API</span>
|
||||
</div>
|
||||
<div class="side-nav-spacer"></div>
|
||||
</div>
|
||||
<div id="sidenav-cover" class="click-through"></div>
|
||||
@ -122,4 +136,5 @@
|
||||
<component v-bind:is="currentViewModal">
|
||||
</component>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
18
public/css/admin.css
Normal file
18
public/css/admin.css
Normal file
@ -0,0 +1,18 @@
|
||||
.collection-item {
|
||||
display: flow-root;
|
||||
}
|
||||
|
||||
.collection-item:hover {
|
||||
background-color: #EEE;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.collection-item svg {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.collection-item div {
|
||||
vertical-align: middle;
|
||||
height: 32.4px;
|
||||
float: left;
|
||||
}
|
||||
@ -1,3 +1,27 @@
|
||||
const ADMINDATA = (() => {
|
||||
const module = {};
|
||||
module.sharedSelect = { value: '' };
|
||||
module.folders = {};
|
||||
module.foldersUpdated = { ts: 0 };
|
||||
|
||||
module.getFolders = async () => {
|
||||
const res = await API.axios({
|
||||
method: 'GET',
|
||||
url: `${API.url()}/api/v1/admin/directories`
|
||||
});
|
||||
|
||||
Object.keys(res.data.memory).forEach(key=>{
|
||||
module.folders[key] = res.data.memory[key];
|
||||
});
|
||||
|
||||
module.foldersUpdated.ts = Date.now();
|
||||
};
|
||||
|
||||
return module;
|
||||
})();
|
||||
|
||||
// Load in data
|
||||
ADMINDATA.getFolders();
|
||||
|
||||
// initialize modal
|
||||
M.Modal.init(document.querySelectorAll('.modal'), {});
|
||||
@ -5,9 +29,11 @@ M.Modal.init(document.querySelectorAll('.modal'), {});
|
||||
const foldersView = Vue.component('folders-view', {
|
||||
data() {
|
||||
return {
|
||||
componentKey: false, // Flip this value to force re-render,
|
||||
componentKey: false, // Flip this value to force re-render
|
||||
dirName: '',
|
||||
folder: ''
|
||||
folder: ADMINDATA.sharedSelect,
|
||||
foldersTS: ADMINDATA.foldersUpdated,
|
||||
folders: ADMINDATA.folders
|
||||
};
|
||||
},
|
||||
template: `
|
||||
@ -19,14 +45,14 @@ const foldersView = Vue.component('folders-view', {
|
||||
<span class="card-title">Add Folder</span>
|
||||
<form id="choose-directory-form" class="choose-directory-form" @submit.prevent="submitForm">
|
||||
<div class="input-field">
|
||||
<input v-on:click="addFolderDialog()" @blur="maybeResetForm()" v-model="folder" id="folder-name" required type="text" class="validate">
|
||||
<input v-on:click="addFolderDialog()" @blur="maybeResetForm()" v-model="folder.value" id="folder-name" required type="text" class="validate">
|
||||
<label for="folder-name">Select Directory</label>
|
||||
<span class="helper-text">Click to choose directory</span>
|
||||
</div>
|
||||
<div class="input-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">Server Path</label>
|
||||
<span class="helper-text">No special characters</span>
|
||||
<label for="add-directory-name">Server Path Alias (vPath)</label>
|
||||
<span class="helper-text">No special characters or spaces</span>
|
||||
</div>
|
||||
<button class="btn green waves-effect waves-light select-folder-button" type="submit">
|
||||
Add Folder
|
||||
@ -36,18 +62,89 @@ const foldersView = Vue.component('folders-view', {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="foldersTS.ts === 0" class="row">
|
||||
<svg class="spinner" width="65px" height="65px" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg"><circle class="spinner-path" fill="none" stroke-width="6" stroke-linecap="round" cx="33" cy="33" r="30"></circle></svg>
|
||||
</div>
|
||||
<div v-show="foldersTS.ts > 0" class="row">
|
||||
<div class="col s12">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Server Path Alias (vPath)</th>
|
||||
<th>Directory</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(v, k) in folders">
|
||||
<td>{{k}}</td>
|
||||
<td>{{v.root}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>`,
|
||||
created: function() {
|
||||
ADMINDATA.sharedSelect.value = '';
|
||||
},
|
||||
watch: {
|
||||
'folder.value': function (newVal, oldVal) {
|
||||
this.makeVPath(newVal);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
makeVPath(dir) {
|
||||
const newName = dir.split(/[\\\/]/).pop().toLowerCase().replace(' ', '-').replace(/[^a-zA-Z0-9-]/g, "");
|
||||
|
||||
// TODO: Check that vpath doesn't already exist
|
||||
|
||||
this.dirName = newName;
|
||||
this.$nextTick(() => {
|
||||
M.updateTextFields();
|
||||
});
|
||||
},
|
||||
maybeResetForm: function() {
|
||||
if (this.dirName === '' && this.folder === '') {
|
||||
if (this.dirName === '' && this.folder.value === '') {
|
||||
document.getElementById("choose-directory-form").reset();
|
||||
}
|
||||
},
|
||||
addFolderDialog: function (event) {
|
||||
modVM.currentViewModal = 'file-explorer-modal';
|
||||
M.Modal.getInstance(document.getElementById('admin-modal')).open();
|
||||
},
|
||||
submitForm: function (event) {
|
||||
console.log('lol');
|
||||
submitForm: async function () {
|
||||
if (ADMINDATA.folders[this.dirName]) {
|
||||
iziToast.warn({
|
||||
title: 'Server Path already in use',
|
||||
position: 'topCenter',
|
||||
timeout: 3500
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await API.axios({
|
||||
method: 'PUT',
|
||||
url: `${API.url()}/api/v1/admin/directory`,
|
||||
data: {
|
||||
directory: this.folder.value,
|
||||
vpath: this.dirName
|
||||
}
|
||||
});
|
||||
|
||||
Vue.set(ADMINDATA.folders, this.dirName, { root: this.folder.value });
|
||||
this.dirName = '';
|
||||
this.folder.value = '';
|
||||
this.$nextTick(() => {
|
||||
M.updateTextFields();
|
||||
});
|
||||
}catch(err) {
|
||||
iziToast.error({
|
||||
title: 'Failed to add directory',
|
||||
position: 'topCenter',
|
||||
timeout: 3500
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -92,12 +189,119 @@ function changeView(viewName, el){
|
||||
closeSideMenu();
|
||||
}
|
||||
|
||||
const fileExplorerModal = Vue.component('file-explorer-modal', {
|
||||
data() {
|
||||
return {
|
||||
componentKey: false, // Flip this value to force re-render,
|
||||
pending: false,
|
||||
currentDirectory: null,
|
||||
contents: []
|
||||
};
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div class="row">
|
||||
<h5>File Explorer</h5>
|
||||
<span>
|
||||
[<a v-on:click="goToDirectory(currentDirectory, '..')">back</a>]
|
||||
[<a v-on:click="goToDirectory('~')">home</a>]
|
||||
[<a v-on:click="goToDirectory(currentDirectory)">refresh</a>]
|
||||
</span>
|
||||
</div>
|
||||
<div v-show="currentDirectory === null || pending === true" class="row">
|
||||
<svg class="spinner" width="65px" height="65px" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg"><circle class="spinner-path" fill="none" stroke-width="6" stroke-linecap="round" cx="33" cy="33" r="30"></circle></svg>
|
||||
</div>
|
||||
<div v-show="currentDirectory !== null" class="row">
|
||||
<h6>{{currentDirectory}}</h6>
|
||||
[<a v-on:click="selectDirectory(currentDirectory)">Select Current Directory</a>]
|
||||
<ul class="collection">
|
||||
<li v-on:click="goToDirectory(currentDirectory, dir.name)" v-for="dir in contents" class="collection-item">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" height="32.4px"><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>{{dir.name}}</div>
|
||||
<a v-on:click.stop="selectDirectory(currentDirectory, dir.name)" class="secondary-content waves-effect waves-light btn-small">Select</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>`,
|
||||
created: async function () {
|
||||
this.goToDirectory('~');
|
||||
},
|
||||
methods: {
|
||||
goToDirectory: async function (dir, joinDir) {
|
||||
try {
|
||||
const params = { directory: dir };
|
||||
if (joinDir) { params.joinDirectory = joinDir; }
|
||||
|
||||
const res = await API.axios({
|
||||
method: 'POST',
|
||||
url: `${API.url()}/api/v1/admin/file-explorer`,
|
||||
data: params
|
||||
});
|
||||
|
||||
this.currentDirectory = res.data.path
|
||||
|
||||
while (this.contents.length > 0) {
|
||||
this.contents.pop();
|
||||
}
|
||||
|
||||
res.data.directories.forEach(d => {
|
||||
this.contents.push(d);
|
||||
});
|
||||
|
||||
this.$nextTick(() => {
|
||||
document.getElementById('dynamic-modal').scrollIntoView();
|
||||
});
|
||||
} catch(err) {
|
||||
iziToast.error({
|
||||
title: 'Failed to get directory contents',
|
||||
position: 'topCenter',
|
||||
timeout: 3500
|
||||
});
|
||||
}
|
||||
},
|
||||
selectDirectory: async function (dir, joinDir) {
|
||||
try {
|
||||
let selectThis = dir;
|
||||
|
||||
if (joinDir) {
|
||||
const res = await API.axios({
|
||||
method: 'POST',
|
||||
url: `${API.url()}/api/v1/admin/file-explorer`,
|
||||
data: { directory: dir, joinDirectory: joinDir }
|
||||
});
|
||||
|
||||
selectThis = res.data.path
|
||||
}
|
||||
|
||||
Vue.set(ADMINDATA.sharedSelect, 'value', selectThis);
|
||||
|
||||
// close the modal
|
||||
M.Modal.getInstance(document.getElementById('admin-modal')).close();
|
||||
// reset the modal
|
||||
modVM.currentViewModal = 'null-modal';
|
||||
}catch(err) {
|
||||
iziToast.error({
|
||||
title: 'Cannot Select Directory',
|
||||
position: 'topCenter',
|
||||
timeout: 3500
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const nullModal = Vue.component('null-modal', {
|
||||
template: '<div>NULL MODAL ERROR: How did you get here?</div>'
|
||||
});
|
||||
|
||||
const modVM = new Vue({
|
||||
el: '#dynamic-modal',
|
||||
components: {
|
||||
'file-explorer-modal': fileExplorerModal,
|
||||
'null-modal': nullModal
|
||||
},
|
||||
data: {
|
||||
currentViewModal: false
|
||||
currentViewModal: 'null-modal'
|
||||
}
|
||||
});
|
||||
|
||||
61
public/js/api.js
Normal file
61
public/js/api.js
Normal file
@ -0,0 +1,61 @@
|
||||
const API = (() => {
|
||||
const module = {};
|
||||
|
||||
// initialize with a default server
|
||||
module.servers = [{
|
||||
name: "default",
|
||||
url: window.location.origin,
|
||||
token: localStorage.getItem('token')
|
||||
}];
|
||||
|
||||
module.selectedServer = 0;
|
||||
|
||||
module.addServer = (name, url, username, password) => {
|
||||
module.servers.push({
|
||||
name: name,
|
||||
url: url,
|
||||
token: null
|
||||
})
|
||||
}
|
||||
|
||||
module.name = () => {
|
||||
return module.servers[module.selectedServer].name;
|
||||
}
|
||||
|
||||
module.token = () => {
|
||||
return module.servers[module.selectedServer].token;
|
||||
}
|
||||
|
||||
module.url = () => {
|
||||
return module.servers[module.selectedServer].url;
|
||||
}
|
||||
|
||||
module.checkAuthAndKickToLogin = async () => {
|
||||
if (module.servers[0].token === null) {
|
||||
window.location.replace(`/login?redirect=${encodeURIComponent(window.location.pathname)}`);
|
||||
}
|
||||
|
||||
// Send request to server
|
||||
try {
|
||||
await axios({
|
||||
method: 'GET',
|
||||
url: `${module.url()}/api/`,
|
||||
headers: { 'x-access-token': module.token() }
|
||||
});
|
||||
} catch (err) {
|
||||
window.location.replace(`/login?redirect=${encodeURIComponent(window.location.pathname)}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.logout = () => {
|
||||
localStorage.removeItem('authToken');
|
||||
window.location.replace(`/login`);
|
||||
}
|
||||
|
||||
module.axios = axios.create({
|
||||
baseURL: module.url(),
|
||||
headers: { 'x-access-token': module.token() }
|
||||
});
|
||||
|
||||
return module;
|
||||
})();
|
||||
2
public/js/lib/axios.js
Normal file
2
public/js/lib/axios.js
Normal file
File diff suppressed because one or more lines are too long
@ -158,6 +158,11 @@ $(document).ready(function () {
|
||||
location.reload();
|
||||
});
|
||||
|
||||
// Admin Panel
|
||||
$(document).on('click', '.admin-panel-button', function (event) {
|
||||
window.open('admin', '_blank');
|
||||
});
|
||||
|
||||
// Dropzone
|
||||
const myDropzone = new Dropzone(document.body, {
|
||||
previewsContainer: false,
|
||||
|
||||
@ -283,6 +283,10 @@
|
||||
|
||||
<div class="left-nav-menu-header">System</div>
|
||||
<ul class="left-nav-menu">
|
||||
<li class="admin-panel-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path clip-rule="evenodd" d="M0 0h24v24H0z" fill="none"/><path d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4z"/></svg>
|
||||
Server Admin
|
||||
</li>
|
||||
<li class="auto_dj_settings">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#DDD" width="24" height="24" viewBox="0 0 55.334 55.334"><g><circle cx="27.667" cy="27.667" r="3.618"/><path d="M27.667 0C12.387 0 0 12.387 0 27.667s12.387 27.667 27.667 27.667 27.667-12.387 27.667-27.667S42.947 0 27.667 0zM17.118 6.881a23.213 23.213 0 0111.214-2.509c.367.01.619.922.564 2.025l-.282 5.677c-.055 1.103-.289 1.986-.523 1.979a13.577 13.577 0 00-6.027 1.196c-1.007.455-2.212.184-2.774-.767l-2.896-4.897c-.562-.951-.261-2.203.724-2.704zm-1.132 10.414l-4.278-3.742c-.832-.727-.918-1.994-.119-2.756l.057-.053c.802-.76 2.059-.605 2.737.266l3.494 4.484c.679.871.837 1.889.391 2.314-.447.427-1.45.214-2.282-.513zm1.891 10.372c0-5.407 4.383-9.79 9.79-9.79s9.79 4.383 9.79 9.79-4.383 9.79-9.79 9.79-9.79-4.383-9.79-9.79zM38.17 48.476a23.21 23.21 0 01-11.244 2.484c-.409-.013-.692-.929-.632-2.032l.31-5.676c.061-1.103.322-1.981.586-1.972a13.596 13.596 0 005.656-1.01c1.022-.42 2.275-.144 2.877.782l3.101 4.77c.602.925.332 2.155-.654 2.654zm5.449-3.82c-.766.72-2.005.551-2.703-.305l-3.59-4.407c-.698-.856-.876-1.848-.435-2.255.442-.407 1.443-.179 2.274.549l4.28 3.744c.832.727.941 1.954.174 2.674z"/></g></svg>
|
||||
Auto DJ
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
const path = require('path');
|
||||
const Joi = require('joi');
|
||||
const fileExplorer = require('../util/file-explorer');
|
||||
const admin = require('../util/admin');
|
||||
|
||||
exports.setup = (mstream, program) => {
|
||||
// The admin file explorer can view the entire system
|
||||
@ -34,7 +35,57 @@ exports.setup = (mstream, program) => {
|
||||
files: folderContents.files
|
||||
});
|
||||
}catch (err) {
|
||||
console.log('XXXX');
|
||||
console.log(err);
|
||||
return res.status(500).json({ error: 'Failed to get directory contents' });
|
||||
}
|
||||
});
|
||||
|
||||
mstream.get("/api/v1/admin/directories", async (req, res) => {
|
||||
try {
|
||||
const config = await admin.loadFile(program.configFile);
|
||||
res.json({ file: config.folders, memory: program.folders });
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
return res.status(500).json({ error: 'Failed to get vpaths' });
|
||||
}
|
||||
});
|
||||
|
||||
mstream.put("/api/v1/admin/directory", async (req, res) => {
|
||||
try {
|
||||
const schema = Joi.object({
|
||||
directory: Joi.string().required(),
|
||||
vpath: Joi.string().pattern(/[a-zA-Z0-9-]+/).required()
|
||||
});
|
||||
await schema.validateAsync(req.body);
|
||||
}catch (err) {
|
||||
console.log(err)
|
||||
return res.status(500).json({ error: 'Validation Error' });
|
||||
}
|
||||
|
||||
try {
|
||||
await admin.addDirectory(req.body.directory, req.body.vpath, program.configFile, program, mstream);
|
||||
res.json({});
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
return res.status(500).json({ error: 'Failed to set new directory' });
|
||||
}
|
||||
});
|
||||
|
||||
// mstream.delete("/api/v1/admin/directory", async (req, res) => {
|
||||
// try {
|
||||
// const schema = Joi.object({
|
||||
// vpath: Joi.string().pattern().required()
|
||||
// });
|
||||
// await schema.validateAsync(req.body);
|
||||
// }catch (err) {
|
||||
// return res.status(500).json({ error: 'Validation Error' });
|
||||
// }
|
||||
|
||||
// try {
|
||||
|
||||
// } catch (err) {
|
||||
|
||||
// }
|
||||
// });
|
||||
}
|
||||
34
src/util/admin.js
Normal file
34
src/util/admin.js
Normal file
@ -0,0 +1,34 @@
|
||||
const fs = require("fs").promises;
|
||||
const express = require('express');
|
||||
|
||||
exports.loadFile = async function(file) {
|
||||
return JSON.parse(await fs.readFile(file, 'utf-8'));
|
||||
}
|
||||
|
||||
exports.saveFile = async function(saveData, file) {
|
||||
return await fs.writeFile(file, JSON.stringify(saveData, null, 2), 'utf8')
|
||||
}
|
||||
|
||||
exports.addDirectory = async function(directory, vpath, configFile, program, mstream) {
|
||||
try {
|
||||
// confirm directory is real
|
||||
const stat = await fs.stat(directory);
|
||||
if (!stat.isDirectory()) { throw 'not a directory' };
|
||||
|
||||
const config = await this.loadFile(configFile);
|
||||
config.folders[vpath] = { root: directory };
|
||||
|
||||
await this.saveFile(config, configFile);
|
||||
|
||||
program.folders[vpath] = { root: directory };
|
||||
|
||||
if (mstream) {
|
||||
mstream.use(`/media/${vpath}/`, express.static(directory));
|
||||
}
|
||||
}catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
exports.deleteDirectory = async function(vpath, configFile, program, mstream) {
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user