some admin panel work

This commit is contained in:
Evert Prants 2017-12-04 20:20:53 +02:00
parent 5575e9e4f6
commit c9f75fda39
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
11 changed files with 2595 additions and 1830 deletions

3693
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,8 +8,8 @@
"test": "echo \"Error: no test specified\" && exit 1",
"css": "mkdir -p build/style && stylus -o build/style src/style/*.styl",
"css:watch": "mkdir -p build/style && stylus -w -o build/style src/style/*.styl",
"js": "webpack",
"js:watch": "webpack -w",
"js": "webpack --config webpack.prod.js -p",
"js:watch": "webpack --config webpack.dev.js -w",
"watch": "concurrently --kill-others \"npm run css:watch\" \"npm run js:watch\"",
"clean": "rm -rf build/",
"build": "npm run clean && npm run css && npm run js"
@ -30,43 +30,48 @@
},
"homepage": "https://github.com/IcyNet/IcyNet.eu#readme",
"dependencies": {
"babel-core": "^6.25.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.24.1",
"babel-core": "^6.26.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
"bcryptjs": "^2.4.3",
"bluebird": "^3.5.0",
"body-parser": "^1.17.2",
"connect-redis": "^3.3.0",
"connect-session-knex": "^1.3.4",
"bluebird": "^3.5.1",
"body-parser": "^1.18.2",
"connect-redis": "^3.3.2",
"connect-session-knex": "^1.4.0",
"email-templates": "^2.7.1",
"express": "^4.15.3",
"express": "^4.16.2",
"express-rate-limit": "^2.9.0",
"express-session": "^1.15.3",
"express-session": "^1.15.6",
"fs-extra": "^4.0.2",
"gm": "^1.23.0",
"knex": "^0.13.0",
"multiparty": "^4.1.3",
"mysql": "^2.13.0",
"nodemailer": "^4.0.1",
"mysql": "^2.15.0",
"nodemailer": "^4.4.0",
"notp": "^2.0.3",
"oauth-libre": "^0.9.17",
"objection": "^0.8.4",
"pug": "^2.0.0-rc.3",
"serve-favicon": "^2.4.3",
"objection": "^0.8.9",
"pug": "^2.0.0-rc.4",
"serve-favicon": "^2.4.5",
"stylus": "^0.54.5",
"thirty-two": "^1.0.2",
"toml": "^2.3.2",
"uuid": "^3.1.0"
"toml": "^2.3.3",
"uuid": "^3.1.0",
"vue": "^2.5.9"
},
"devDependencies": {
"concurrently": "^3.5.0",
"eslint-plugin-import": "^2.7.0",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.1",
"concurrently": "^3.5.1",
"eslint-plugin-import": "^2.8.0",
"jquery": "^3.2.1",
"morgan": "^1.9.0",
"mustache": "^2.3.0",
"standard": "^10.0.3",
"uglifyjs-webpack-plugin": "^0.4.6",
"vue-resource": "^1.3.4",
"watch": "^1.0.2",
"webpack": "^3.6.0"
"webpack": "^3.10.0",
"webpack-merge": "^4.1.1"
},
"standard": {
"env": {

View File

@ -1,284 +1,122 @@
window.$ = require('jquery')
var Mustache = require('mustache')
import Vue from 'vue'
import VueResource from 'vue-resource'
function buildTemplateScript (id, ctx) {
var tmpl = $('#' + id)
if (!tmpl.length) return null
var data = tmpl.html()
Mustache.parse(data)
return Mustache.render(data, ctx)
}
const csrfToken = document.querySelector('meta[name="csrf-token"]').content
function paginationButton (pages) {
var html = '<div class="pgn">'
html += '<span class="pagenum">Page ' + pages.page + ' of ' + pages.pages + '</span>'
Vue.use(VueResource)
if (pages.page > 1) {
html += '<div class="button" data-page="' + (pages.page - 1) + '">Previous</div>'
}
for (var i = 0; i < pages.pages; i++) {
html += '<div class="button' + (i + 1 === pages.page ? ' active' : '') + '" data-page="' + (i + 1) + '">' + (i + 1) + '</div>'
}
if (pages.pages > pages.page) {
html += '<div class="button" data-page="' + (pages.page + 1) + '">Next</div>'
}
html += '<span class="pagenum total">(' + pages.total + ' Total Entries)</span>'
html += '</div>'
return html
}
function banUser (id) {
window.Dialog.openTemplate('Ban User', 'banNew', {id: id})
$('#fnsubmit').submit(function (e) {
e.preventDefault()
$.post({
url: '/admin/api/ban',
data: $(this).serialize(),
success: function (data) {
window.Dialog.close()
loadBans(1)
},
error: function (e) {
if (e.responseJSON && e.responseJSON.error) {
$('form .message').show()
$('form .message').text(e.responseJSON.error)
}
Vue.component('Modal', {
template: '#modal-template',
props: ['show'],
methods: {
close: function () {
this.$emit('close')
}
},
mounted: function () {
document.addEventListener('keydown', (e) => {
if (this.show && e.keyCode === 27) {
this.close()
}
})
})
}
function loadBans (page) {
$.ajax({
type: 'get',
url: '/admin/api/bans',
data: {page: page},
success: function (data) {
$('#banlist').html('')
if (data.error) {
$('#banlist').html('<div class="message">' + data.error + '</div>')
return
}
var pgbtn = paginationButton(data.page)
$('#banlist').append(pgbtn)
$('#banlist .pgn .button').click(function (e) {
var pgnum = $(this).data('page')
if (pgnum == null) return
loadBans(parseInt(pgnum))
})
for (var u in data.bans) {
var ban = data.bans[u]
ban.created_at = new Date(ban.created_at)
ban.expires_at = ban.expires_at === null ? 'Never' : new Date(ban.expires_at)
var tmp = buildTemplateScript('ban', ban)
$('#banlist').append(tmp)
}
$('#banlist .remove').click(function (e) {
$.post({
url: '/admin/api/ban/pardon/' + parseInt($(this).data('id')),
success: function (data) {
loadBans(1)
}
})
})
}
})
}
function loadUsers (page) {
$.ajax({
type: 'get',
url: '/admin/api/users',
data: {page: page},
success: function (data) {
$('#userlist').html('')
if (data.error) {
$('#userlist').html('<div class="message error">' + data.error + '</div>')
return
}
var pgbtn = paginationButton(data.page)
$('#userlist').append(pgbtn)
$('#userlist .pgn .button').click(function (e) {
var pgnum = $(this).data('page')
if (pgnum == null) return
loadUsers(parseInt(pgnum))
})
for (var u in data.users) {
var user = data.users[u]
user.created_at = new Date(user.created_at)
var tmp = buildTemplateScript('user', user)
$('#userlist').append(tmp)
}
$('#userlist .ban').click(function (e) {
banUser(parseInt($(this).data('id')))
})
}
})
}
function editClient (id) {
$.ajax({
type: 'get',
url: '/admin/api/client/' + id,
success: function (data) {
window.Dialog.openTemplate('Editing client', 'clientEdit', data)
$('#ffsubmit').submit(function (e) {
e.preventDefault()
$.ajax({
type: 'post',
url: '/admin/api/client/update',
data: $(this).serialize(),
success: function (data) {
window.Dialog.close()
loadClients(1)
},
error: function (e) {
if (e.responseJSON && e.responseJSON.error) {
$('form .message').show()
$('form .message').text(e.responseJSON.error)
}
}
})
})
}
})
}
function deleteClient (id) {
window.Dialog.openTemplate('Deleting client', 'clientRemove')
$('#fremove').click(function (e) {
$.post({
url: '/admin/api/client/delete/' + id,
success: function (data) {
window.Dialog.close()
loadClients(1)
}
})
})
}
function loadClients (page) {
$.ajax({
type: 'get',
url: '/admin/api/clients',
data: {page: page},
success: function (data) {
$('#clientlist').html('')
if (data.error) {
$('#clientlist').html('<div class="message error">' + data.error + '</div>')
return
}
var pgbtn = paginationButton(data.page)
$('#clientlist').append(pgbtn)
$('#clientlist .pgn .button').click(function (e) {
var pgnum = $(this).data('page')
if (pgnum == null) return
loadClients(parseInt(pgnum))
})
for (var u in data.clients) {
var client = data.clients[u]
client.created_at = new Date(client.created_at)
var tmp = buildTemplateScript('client', client)
$('#clientlist').append(tmp)
}
$('#clientlist .edit').click(function (e) {
var client = $(this).data('client')
editClient(parseInt(client))
})
$('#clientlist .delete').click(function (e) {
var client = $(this).data('client')
deleteClient(parseInt(client))
})
$('#clientlist .newsecret').click(function (e) {
var client = $(this).data('client')
$.post({
url: '/admin/api/client/new_secret/' + parseInt(client),
success: function (e) {
loadClients(1)
}
})
})
}
})
}
$(document).ready(function () {
window.Dialog = $('#dialog')
window.Dialog.open = function (title, content, pad) {
$('#dialog #title').text(title)
if (pad) {
content = '<div class="pad">' + content + '</div>'
}
$('#dialog #content').html(content)
$('#dialog').fadeIn()
}
window.Dialog.close = function () {
$('#dialog').fadeOut('fast', function () {
$('#dialog #content').html('')
})
}
window.Dialog.openTemplate = function (title, template, data) {
window.Dialog.open(title, buildTemplateScript(template, data || {}), true)
}
$('#dialog #close').click(function (e) {
window.Dialog.close()
})
if ($('#userlist').length) {
loadUsers(1)
}
if ($('#banlist').length) {
loadBans(1)
}
if ($('#clientlist').length) {
loadClients(1)
$('#new').click(function (e) {
window.Dialog.openTemplate('New Client', 'clientNew')
$('#fnsubmit').submit(function (e) {
e.preventDefault()
$.post({
url: '/admin/api/client/new',
data: $(this).serialize(),
success: function (data) {
window.Dialog.close()
loadClients(1)
},
error: function (e) {
if (e.responseJSON && e.responseJSON.error) {
$('form .message').show()
$('form .message').text(e.responseJSON.error)
}
}
})
})
})
}
setInterval(function () {
$.get({
url: '/admin/access'
}).fail(function () {
window.location.reload()
})
}, 30000)
})
Vue.component('BanModal', {
template: '#ban-modal-template',
props: ['show', 'id'],
data: function () {
return {
error: '',
reason: '',
expires_at: null
}
},
methods: {
close: function () {
this.$emit('close')
this.error = ''
this.reason = ''
this.expires_at = null
},
submit: function () {
this.$http.post('/admin/api/ban', {
user_id: this.id,
reason: this.reason,
expires_at: this.expires_at,
csrf: csrfToken
}).then(data => {
this.close()
banList.getBans(1)
}).catch(err => {
console.log(err)
if (err.body && err.body.error) this.error = err.body.error
})
}
}
})
const userList = new Vue({
el: '#userlist',
data: {
pagination: {
offset: 0,
page: 1,
pages: 1,
perPage: 6,
total: 0
},
users: [],
banning: 0
},
mounted: function () {
this.getUsers(1)
},
methods: {
getUsers: function (page) {
this.$http.get('/admin/api/users?page=' + page).then(data => {
if (data.body && data.body.error) return
this.pagination = data.body.page
this.users = data.body.users
})
}
}
})
const banList = new Vue({
el: '#banlist',
data: {
pagination: {
offset: 0,
page: 1,
pages: 1,
perPage: 6,
total: 0
},
error: '',
bans: []
},
mounted: function () {
this.getBans(1)
},
methods: {
getBans: function (page) {
this.error = ''
this.pagination.total = 0
this.$http.get('/admin/api/bans?page=' + page).then(data => {
if (data.body && data.body.error) {
this.error = data.body.error
return
}
this.pagination = data.body.page
this.bans = data.body.bans
})
},
pardon: function (id) {
this.$http.post('/admin/api/ban/pardon/' + id).then(data => {
this.getBans(1)
})
}
}
})

View File

@ -55,6 +55,21 @@ nav
.username
font-size: 80%
.info
.section
margin: 5px 0
.key
width: 120px
display: inline-block
.list
padding: 10px
.list-item
background-color: #f5f5f5
padding: 10px
&:nth-child(even)
background-color: #fff
.application
height: 200px
#hiddensecret
@ -66,6 +81,63 @@ nav
cursor: pointer
display: inline-block
.modal-mask
position: fixed
z-index: 9998
top: 0
left: 0
width: 100%
height: 100%
background-color: rgba(0, 0, 0, .5)
display: table
transition: opacity .3s ease
.modal-wrapper
display: table-cell
vertical-align: middle
.modal-container
width: 300px
margin: 0px auto
padding: 20px 30px
background-color: #fff
border-radius: 2px
box-shadow: 0 2px 8px rgba(0, 0, 0, .33)
transition: all .3s ease
font-family: Helvetica, Arial, sans-serif
.modal-header h3
margin-top: 0
color: #42b983
.modal-body
margin: 20px 0
.modal-default-button
float: right
/*
* The following styles are auto-applied to elements with
* transition="modal" when their visibility is toggled
* by Vue.js.
*
* You can easily play with the modal transition by editing
* these styles.
*/
.modal-enter
opacity: 0
.modal-leave-active
opacity: 0
.modal-enter .modal-container, .modal-leave-active .modal-container
-webkit-transform: scale(1.1)
transform: scale(1.1)
.modal-footer
min-height: 50px
form
.message
display: none

View File

@ -197,7 +197,7 @@ input:not([type="submit"])
box-shadow: inset 2px 2px 5px #ddd
transition: border 0.1s linear
.button, input[type="submit"]
button, .button, input[type="submit"]
display: block
padding: 5px 10px
background-color: #fbfbfb

View File

@ -5,65 +5,95 @@ block body
.content
h1 Welcome to the Admin Panel
.left
.users
h3 Registered Users
#userlist
#userlist
h3 Registered Users ({{ pagination.total }})
.pgn
span.pagenum Page {{ pagination.page }} of {{ pagination.pages }}
.button(v-if="pagination.page > 1" v-on:click="getUsers(pagination.page - 1)") Previous
.button(v-for="n in pagination.pages" v-on:click="getUsers(n)" v-bind:class="{active: n == pagination.page}") {{ n }}
.button(v-if="pagination.page < pagination.pages" v-on:click="getUsers(pagination.page + 1)") Next
.list.users
.user.list-item(v-for="user in users")
.avatar
img(v-if="user.avatar_file" v-bind:src="'/usercontent/images/' + user.avatar_file")
img(v-else src="/static/image/avatar.png")
.info
.stamps
.noactive(v-if="user.activated == false" title="Not activated.")
i.fa.fa-fw.fa-envelope
.display_name {{ user.display_name }}
.username {{ user.id }} - {{ user.username }} ({{ user.uuid }})
.email {{ user.email }}
.privilege Privilege: level {{ user.nw_privilege }}
.timestamp {{ new Date(user.created_at).toString() }}
.external(v-if="!user.password")
b Used external login
.button.ban(v-if="user.bannable" v-on:click="banning = user.id")
i.fa.fa-fw.fa-ban
|Ban User
ban-modal(:show="banning", @close="banning = 0", :id="banning")
.right
.users
h3 Bans
#banlist
#banlist
h3 Bans ({{pagination.total}})
.message.error(v-if="error") {{ error }}
.entry(v-else)
.pgn
span.pagenum Page {{ pagination.page }} of {{ pagination.pages }}
.button(v-if="pagination.page > 1" v-on:click="getBans(pagination.page - 1)") Previous
.button(v-for="n in pagination.pages" v-on:click="getBans(n)" v-bind:class="{active: n == pagination.page}") {{ n }}
.button(v-if="pagination.page < pagination.pages" v-on:click="getBans(pagination.page + 1)") Next
.list.bans
.ban.list-item(v-for="ban in bans")
.stamps
.noactive(title="Expired" v-if="ban.expired")
.fa.fa-fw.fa-ban
.info
.section
span.key User
span.value {{ ban.user.display_name }}
.section
span.key Admin
span.value {{ ban.admin.display_name }}
.section
span.key Reason
span.value {{ ban.reason }}
.section
span.key Placed
span.value {{ new Date(ban.created_at).toString() }}
.section
span.key Expires
span.value(v-if="ban.expires_at") {{ new Date(ban.expires_at).toString() }}
span.value(v-else)
b This ban is permanent.
.button.remove(@click="pardon(ban.id)") Pardon
.templates
script(type="x-tmpl-mustache" id="user").
<div class="user" id="user-{{id}}">
<div class="avatar">
{{#avatar_file}}
<img src="/usercontent/images/{{avatar_file}}">
{{/avatar_file}}
{{^avatar_file}}
<img src="/static/image/avatar.png">
{{/avatar_file}}
</div>
<div class="info">
<div class="stamps">
{{^activated}}
<div class="noactive" title="Not activated"><i class="fa fa-fw fa-envelope"></i></div>
{{/activated}}
script(type="text/x-template" id="modal-template").
<transition name="modal">
<div class="modal-mask" @click="close" v-show="show">
<div class="modal-wrapper" @click.stop>
<div class="modal-container">
<slot></slot>
</div>
</div>
</div>
<div class="display_name">{{display_name}}</div>
<div class="username">{{id}} - {{username}} ({{uuid}})</div>
<div class="email">{{email}}</div>
<div class="privilege">Privilege: level {{nw_privilege}}</div>
<div class="timestamp">{{created_at}}</div>
{{^password}}
<div class="external"><b>Used external login</b></div>
{{/password}}
{{#bannable}}
<div class="button ban" data-id="{{id}}"><i class="fa fa-fw fa-ban"></i>Ban User</div>
{{/bannable}}
</div>
</div>
script(type="x-tmpl-mustache" id="ban").
<div class="ban" id="ban-{{user.id}}">
<div class="stamps">
{{#expired}}
<div class="noactive" title="Expired"><i class="fa fa-fw fa-ban"></i></div>
{{/expired}}
</transition>
script(type="text/x-template" id="ban-modal-template").
<modal :show="show" @close="close">
<div class="modal-header">
<h3>Ban user</h3>
</div>
<div class="display_name">User: {{user.display_name}}</div>
<div class="display_name">Admin: {{admin.display_name}}</div>
<div class="description">Reason: {{reason}}</div>
<div class="timestamp">Placed {{created_at}}</div>
<div class="timestamp">Expires {{expires_at}}</div>
<div class="button remove" data-id="{{id}}">Pardon</div>
</div>
script(type="x-tmpl-mustache" id="banNew").
<form id="fnsubmit">
<div class="message error"></div>
<input type="hidden" name="csrf" value="#{csrf}">
<input type="hidden" name="user_id" value="{{id}}">
<label for="reason">Reason</label>
<input type="text" id="reason" name="reason">
<label for="expires_at">Expires</label>
<input type="date" id="expires_at" name="expires_at">
<input type="submit" value="Create">
</form>
<div class="modal-body">
<div class="message error" v-if="error">{{ error }}</div>
<input type="hidden" name="user_id" :value="id">
<label for="reason">Reason</label>
<input type="text" id="reason" name="reason" v-model="reason">
<label for="expires_at">Expires</label>
<input type="date" id="expires_at" name="expires_at" v-model="expires_at">
</div>
<div class="modal-footer text-right">
<button @click="submit">Ban</button>
<button @click="close">Cancel</button>
</div>
</modal>

View File

@ -1,6 +1,7 @@
html
head
meta(charset="utf8")
meta(name="csrf-token", content=csrf)
block links
link(rel="stylesheet", type="text/css", href="https://fonts.googleapis.com/css?family=Open+Sans")
link(rel="stylesheet", type="text/css", href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css")
@ -10,7 +11,6 @@ html
window.variables = {
server_time: parseInt('#{server_time}')
};
script(src="/script/admin.js")
title
block title
|Icy Network - Administration
@ -37,3 +37,4 @@ html
.content#content
.wrapper
block body
script(src="/script/admin.js")

37
webpack.common.js Normal file
View File

@ -0,0 +1,37 @@
const path = require('path')
module.exports = {
entry: {
main: './src/script/main.js',
admin: './src/script/admin.js'
},
output: {
path: path.join(__dirname, 'build', 'script'),
filename: '[name].js'
},
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js' // 'vue/dist/vue.common.js' for webpack 1
}
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: [
'env'
],
plugins: [
'transform-es2015-modules-commonjs'
]
}
}
}
]
},
plugins: []
}

View File

@ -1,16 +0,0 @@
const path = require('path')
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
entry: {
main: './src/script/main.js',
admin: './src/script/admin.js'
},
output: {
path: path.join(__dirname, 'build', 'script'),
filename: '[name].js'
},
plugins: [
new UglifyJSPlugin()
]
}

6
webpack.dev.js Normal file
View File

@ -0,0 +1,6 @@
const merge = require('webpack-merge')
const common = require('./webpack.common.js')
module.exports = merge(common, {
devtool: 'inline-source-map'
})

13
webpack.prod.js Normal file
View File

@ -0,0 +1,13 @@
const webpack = require('webpack')
const merge = require('webpack-merge')
const common = require('./webpack.common.js')
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
module.exports = merge(common, {
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
}),
new UglifyJSPlugin()
]
})