Browse Source

Added the majority of the punishment system.

KrisVos130 8 years ago
parent
commit
6803f984c2

+ 2 - 1
backend/logic/actions/index.js

@@ -8,5 +8,6 @@ module.exports = {
 	playlists: require('./playlists'),
 	users: require('./users'),
 	reports: require('./reports'),
-	news: require('./news')
+	news: require('./news'),
+	punishments: require('./punishments')
 };

+ 33 - 0
backend/logic/actions/punishments.js

@@ -0,0 +1,33 @@
+'use strict';
+
+const 	hooks 	= require('./hooks'),
+	 	async 	= require('async'),
+	 	logger 	= require('../logger'),
+	 	utils 	= require('../utils'),
+	 	db 	= require('../db');
+
+module.exports = {
+
+	/**
+	 * Gets all punishments
+	 *
+	 * @param {Object} session - the session object automatically added by socket.io
+	 * @param {Function} cb - gets called with the result
+	 */
+	index: hooks.adminRequired((session, cb) => {
+		async.waterfall([
+			(next) => {
+				db.models.punishment.find({}, next);
+			}
+		], (err, punishments) => {
+			if (err) {
+				err = utils.getError(err);
+				logger.error("PUNISHMENTS_INDEX", `Indexing punishments failed. "${err}"`);
+				return cb({ 'status': 'failure', 'message': err});
+			}
+			logger.success("PUNISHMENTS_INDEX", "Indexing punishments successful.");
+			cb({ status: 'success', data: punishments });
+		});
+	}),
+
+};

+ 76 - 1
backend/logic/actions/users.js

@@ -8,6 +8,7 @@ const bcrypt = require('bcrypt');
 const db = require('../db');
 const mail = require('../mail');
 const cache = require('../cache');
+const punishments = require('../punishments');
 const utils = require('../utils');
 const hooks = require('./hooks');
 const sha256 = require('sha256');
@@ -925,5 +926,79 @@ module.exports = {
 				});
 			}
 		});
-	}
+	},
+
+	/**
+	 * Bans a user by userId
+	 *
+	 * @param {Object} session - the session object automatically added by socket.io
+	 * @param {String} value - the user id that is going to be banned
+	 * @param {String} reason - the reason for the ban
+	 * @param {String} expiresAt - the time the ban expires
+	 * @param {Function} cb - gets called with the result
+	 * @param {String} userId - the userId automatically added by hooks
+	 */
+	banUserById: hooks.adminRequired((session, value, reason, expiresAt, cb, userId) => {
+		async.waterfall([
+			(next) => {
+				if (!expiresAt || typeof expiresAt !== 'string') return next('Invalid expire date.');
+				let date = new Date();
+				switch(expiresAt) {
+					case '1h':
+						//expiresAt = date.setHours(date.getHours() + 1);
+						expiresAt = date.setMinutes(date.getMinutes() + 1);
+						break;
+					case '12h':
+						expiresAt = date.setHours(date.getHours() + 12);
+						break;
+					case '1d':
+						expiresAt = date.setDate(date.getDate() + 1);
+						break;
+					case '1w':
+						expiresAt = date.setDate(date.getDate() + 7);
+						break;
+					case '1m':
+						expiresAt = date.setMonth(date.getMonth() + 1);
+						break;
+					case '3m':
+						expiresAt = date.setMonth(date.getMonth() + 3);
+						break;
+					case '6m':
+						expiresAt = date.setMonth(date.getMonth() + 6);
+						break;
+					case '1y':
+						expiresAt = date.setFullYear(date.getFullYear() + 1);
+						break;
+					case 'never':
+						expiresAt = new Date(3093527980800000);
+						break;
+					default:
+						return next('Invalid expire date.');
+				}
+
+				next();
+			},
+
+			(next) => {
+				punishments.addPunishment('banUserId', value, reason, expiresAt, userId, next)
+			},
+
+			(next) => {
+				//TODO Emit to all users with userId as value
+				next();
+			},
+		], (err) => {
+			if (err && err !== true) {
+				err = utils.getError(err);
+				logger.error("BAN_USER_BY_ID", `User ${userId} failed to ban user ${value} with the reason ${reason}. '${err}'`);
+				cb({status: 'failure', message: err});
+			} else {
+				logger.success("BAN_USER_BY_ID", `User ${userId} has successfully banned user ${value} with the reason ${reason}.`);
+				cb({
+					status: 'success',
+					message: 'Successfully banned user.'
+				});
+			}
+		});
+	})
 };

+ 3 - 1
backend/logic/cache/index.js

@@ -18,7 +18,8 @@ const lib = {
 		station: require('./schemas/station'),
 		playlist: require('./schemas/playlist'),
 		officialPlaylist: require('./schemas/officialPlaylist'),
-		song: require('./schemas/song')
+		song: require('./schemas/song'),
+		punishment: require('./schemas/punishment')
 	},
 
 	/**
@@ -126,6 +127,7 @@ const lib = {
 		lib.client.hgetall(table, (err, obj) => {
 			if (err) return typeof cb === 'function' ? cb(err) : null;
 			if (parseJson && obj) Object.keys(obj).forEach((key) => { try { obj[key] = JSON.parse(obj[key]); } catch (e) {} });
+			if (parseJson && !obj) obj = [];
 			cb(null, obj);
 		});
 	},

+ 5 - 0
backend/logic/cache/schemas/punishment.js

@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (punishment) => {
+	return punishment;
+};

+ 4 - 2
backend/logic/db/index.js

@@ -42,7 +42,8 @@ let lib = {
 				user: new mongoose.Schema(require(`./schemas/user`)),
 				playlist: new mongoose.Schema(require(`./schemas/playlist`)),
 				news: new mongoose.Schema(require(`./schemas/news`)),
-				report: new mongoose.Schema(require(`./schemas/report`))
+				report: new mongoose.Schema(require(`./schemas/report`)),
+				punishment: new mongoose.Schema(require(`./schemas/punishment`))
 			};
 
 			lib.models = {
@@ -52,7 +53,8 @@ let lib = {
 				user: mongoose.model('user', lib.schemas.user),
 				playlist: mongoose.model('playlist', lib.schemas.playlist),
 				news: mongoose.model('news', lib.schemas.news),
-				report: mongoose.model('report', lib.schemas.report)
+				report: mongoose.model('report', lib.schemas.report),
+				punishment: mongoose.model('punishment', lib.schemas.punishment)
 			};
 
 			lib.schemas.user.path('username').validate((username) => {

+ 9 - 0
backend/logic/db/schemas/punishment.js

@@ -0,0 +1,9 @@
+module.exports = {
+	type: { type: String, enum: ["banUserId", "banUserIp"], required: true },
+	value: { type: String, required: true },
+	reason: { type: String, required: true, default: 'Unknown' },
+	active: { type: Boolean, required: true, default: true },
+	expiresAt: { type: Date, required: true },
+	punishedAt: { type: Date, default: Date.now(), required: true },
+	punishedBy: { type: String, required: true }
+};

+ 92 - 66
backend/logic/io.js

@@ -9,6 +9,7 @@ const cache = require('./cache');
 const utils = require('./utils');
 const db = require('./db');
 const logger = require('./logger');
+const punishments = require('./punishments');
 
 module.exports = {
 
@@ -22,6 +23,8 @@ module.exports = {
 			let cookies = socket.request.headers.cookie;
 			let SID = utils.cookies.parseCookies(cookies).SID;
 
+			socket.ip = socket.request.headers['x-forwarded-for'] || '0.0.0.0';
+
 			async.waterfall([
 				(next) => {
 					if (!SID) return next('No SID.');
@@ -35,6 +38,24 @@ module.exports = {
 					session.refreshDate = Date.now();
 					socket.session = session;
 					cache.hset('sessions', SID, session, next);
+				},
+				(res, next) => {
+					punishments.getPunishments((err, punishments) => {
+						const isLoggedIn = !!(socket.session && socket.session.refreshDate);
+						const userId = (isLoggedIn) ? socket.session.userId : null;
+						let banned = false;
+						punishments.forEach((punishment) => {
+							if (punishment.type === 'banUserId' && isLoggedIn && punishment.value === userId) {
+								banned = true;
+							}
+							if (punishment.type === 'banUserIp' && punishment.value === socket.ip) {
+								banned = true;
+							}
+						});
+						//socket.banned = banned;
+						socket.banned = banned;
+						next();
+					});
 				}
 			], () => {
 				if (!socket.session) {
@@ -45,77 +66,82 @@ module.exports = {
 		});
 
 		this.io.on('connection', socket => {
-			socket.ip = socket.request.headers['x-forwarded-for'] || '0.0.0.0';
 			let sessionInfo = '';
 			if (socket.session.sessionId) sessionInfo = ` UserID: ${socket.session.userId}.`;
-			logger.info('IO_CONNECTION', `User connected. IP: ${socket.ip}.${sessionInfo}`);
-
-			// catch when the socket has been disconnected
-			socket.on('disconnect', (reason) => {
-				let sessionInfo = '';
-				if (socket.session.sessionId) sessionInfo = ` UserID: ${socket.session.userId}.`;
-				logger.info('IO_DISCONNECTION', `User disconnected. IP: ${socket.ip}.${sessionInfo}`);
-			});
-
-			// catch errors on the socket (internal to socket.io)
-			socket.on('error', err => console.error(err));
-
-			// have the socket listen for each action
-			Object.keys(actions).forEach((namespace) => {
-				Object.keys(actions[namespace]).forEach((action) => {
-
-					// the full name of the action
-					let name = `${namespace}.${action}`;
-
-					// listen for this action to be called
-					socket.on(name, function () {
-
-						let args = Array.prototype.slice.call(arguments, 0, -1);
-						let cb = arguments[arguments.length - 1];
-
-						// load the session from the cache
-						cache.hget('sessions', socket.session.sessionId, (err, session) => {
-							if (err && err !== true) {
-								if (typeof cb === 'function') return cb({
-									status: 'error',
-									message: 'An error occurred while obtaining your session'
-								});
-							}
-
-							// make sure the sockets sessionId isn't set if there is no session
-							if (socket.session.sessionId && session === null) delete socket.session.sessionId;
+			if (socket.banned) {
+				logger.info('IO_BANNED_CONNECTION', `A user tried to connect, but is currently banned. IP: ${socket.ip}.${sessionInfo}`);
+				socket.emit('keep.me.isBanned');
+				socket.disconnect(true);
+			} else {
+				logger.info('IO_CONNECTION', `User connected. IP: ${socket.ip}.${sessionInfo}`);
+
+				// catch when the socket has been disconnected
+				socket.on('disconnect', (reason) => {
+					let sessionInfo = '';
+					if (socket.session.sessionId) sessionInfo = ` UserID: ${socket.session.userId}.`;
+					logger.info('IO_DISCONNECTION', `User disconnected. IP: ${socket.ip}.${sessionInfo}`);
+				});
+
+				// catch errors on the socket (internal to socket.io)
+				socket.on('error', err => console.error(err));
+
+				// have the socket listen for each action
+				Object.keys(actions).forEach((namespace) => {
+					Object.keys(actions[namespace]).forEach((action) => {
+
+						// the full name of the action
+						let name = `${namespace}.${action}`;
+
+						// listen for this action to be called
+						socket.on(name, function () {
+
+							let args = Array.prototype.slice.call(arguments, 0, -1);
+							let cb = arguments[arguments.length - 1];
+
+							// load the session from the cache
+							cache.hget('sessions', socket.session.sessionId, (err, session) => {
+								if (err && err !== true) {
+									if (typeof cb === 'function') return cb({
+										status: 'error',
+										message: 'An error occurred while obtaining your session'
+									});
+								}
 
-							// call the action, passing it the session, and the arguments socket.io passed us
-							actions[namespace][action].apply(null, [socket.session].concat(args).concat([
-								(result) => {
-									// respond to the socket with our message
-									if (typeof cb === 'function') return cb(result);
+								// make sure the sockets sessionId isn't set if there is no session
+								if (socket.session.sessionId && session === null) delete socket.session.sessionId;
+
+								// call the action, passing it the session, and the arguments socket.io passed us
+								actions[namespace][action].apply(null, [socket.session].concat(args).concat([
+									(result) => {
+										// respond to the socket with our message
+										if (typeof cb === 'function') return cb(result);
+									}
+								]));
+							});
+						})
+					})
+				});
+
+				if (socket.session.sessionId) {
+					cache.hget('sessions', socket.session.sessionId, (err, session) => {
+						if (err && err !== true) socket.emit('ready', false);
+						else if (session && session.userId) {
+							db.models.user.findOne({ _id: session.userId }, (err, user) => {
+								if (err || !user) return socket.emit('ready', false);
+								let role = '';
+								let username = '';
+								let userId = '';
+								if (user) {
+									role = user.role;
+									username = user.username;
+									userId = session.userId;
 								}
-							]));
-						});
+								socket.emit('ready', true, role, username, userId);
+							});
+						} else socket.emit('ready', false);
 					})
-				})
-			});
-
-			if (socket.session.sessionId) {
-				cache.hget('sessions', socket.session.sessionId, (err, session) => {
-					if (err && err !== true) socket.emit('ready', false);
-					else if (session && session.userId) {
-						db.models.user.findOne({ _id: session.userId }, (err, user) => {
-							if (err || !user) return socket.emit('ready', false);
-							let role = '';
-							let username = '';
-							let userId = '';
-							if (user) {
-								role = user.role;
-								username = user.username;
-								userId = session.userId;
-							}
-							socket.emit('ready', true, role, username, userId);
-						});
-					} else socket.emit('ready', false);
-				})
-			} else socket.emit('ready', false);
+				} else socket.emit('ready', false);
+			}
 		});
 
 		cb();

+ 218 - 0
backend/logic/punishments.js

@@ -0,0 +1,218 @@
+'use strict';
+
+const cache = require('./cache');
+const db = require('./db');
+const io = require('./io');
+const utils = require('./utils');
+const async = require('async');
+const mongoose = require('mongoose');
+
+module.exports = {
+
+	/**
+	 * Initializes the punishments module, and exits if it is unsuccessful
+	 *
+	 * @param {Function} cb - gets called once we're done initializing
+	 */
+	init: cb => {
+		async.waterfall([
+			(next) => {
+				cache.hgetall('punishments', next);
+			},
+
+			(punishments, next) => {
+				if (!punishments) return next();
+				let punishmentIds = Object.keys(punishments);
+				async.each(punishmentIds, (punishmentId, next) => {
+					db.models.punishment.findOne({_id: punishmentId}, (err, punishment) => {
+						if (err) next(err);
+						else if (!punishment) cache.hdel('punishments', punishmentId, next);
+						else next();
+					});
+				}, next);
+			},
+
+			(next) => {
+				db.models.punishment.find({}, next);
+			},
+
+			(punishments, next) => {
+				async.each(punishments, (punishment, next) => {
+					if (punishment.active === false || punishment.expiresAt < Date.now()) return next();
+					cache.hset('punishments', punishment._id, cache.schemas.punishment(punishment), next);
+				}, next);
+			}
+		], (err) => {
+			if (err) {
+				console.log(`FAILED TO INITIALIZE PUNISHMENTS. ABORTING. "${err.message}"`);
+				process.exit();
+			} else cb();
+		});
+	},
+
+	/**
+	 * Gets all punishments in the cache that are active, and removes those that have expired
+	 *
+	 * @param {Function} cb - gets called once we're done initializing
+	 */
+	getPunishments: function(cb) {
+		let punishmentsToRemove = [];
+		async.waterfall([
+			(next) => {
+				cache.hgetall('punishments', next);
+			},
+
+			(punishmentsObj, next) => {
+				let punishments = [];
+				for (let id in punishmentsObj) {
+					let obj = punishmentsObj[id];
+					obj.punishmentId = id;
+					punishments.push(obj);
+				}
+				punishments = punishments.filter((punishment) => {
+					if (punishment.expiresAt < Date.now()) punishmentsToRemove.push(punishment);
+					return punishment.expiresAt > Date.now();
+				});
+				next(null, punishments);
+			},
+
+			(punishments, next) => {
+				async.each(
+					punishmentsToRemove,
+					(punishment, next2) => {
+						cache.hdel('punishments', punishment.punishmentId, () => {
+							next2();
+						});
+					},
+					() => {
+						next(null, punishments);
+					}
+				);
+			}
+		], (err, punishments) => {
+			if (err && err !== true) return cb(err);
+
+			cb(null, punishments);
+		});
+	},
+
+	/**
+	 * Gets a punishment by id
+	 *
+	 * @param {String} id - the id of the punishment we are trying to get
+	 * @param {Function} cb - gets called once we're done initializing
+	 */
+	getPunishment: function(id, cb) {
+		async.waterfall([
+
+			(next) => {
+				if (!mongoose.Types.ObjectId.isValid(id)) return next('Id is not a valid ObjectId.');
+				cache.hget('punishments', id, next);
+			},
+
+			(punishment, next) => {
+				if (punishment) return next(true, punishment);
+				db.models.punishment.findOne({_id: id}, next);
+			},
+
+			(punishment, next) => {
+				if (punishment) {
+					cache.hset('punishments', id, punishment, next);
+				} else next('Punishment not found.');
+			},
+
+		], (err, punishment) => {
+			if (err && err !== true) return cb(err);
+
+			cb(null, punishment);
+		});
+	},
+
+	/**
+	 * Gets all punishments from a userId
+	 *
+	 * @param {String} userId - the userId of the punishment(s) we are trying to get
+	 * @param {Function} cb - gets called once we're done initializing
+	 */
+	getPunishmentsFromUserId: function(userId, cb) {
+		async.waterfall([
+			(next) => {
+				module.exports.getPunishments(next);
+			},
+			(punishments, next) => {
+				punishments = punishments.filter((punishment) => {
+					return punishment.type === 'banUserId' && punishment.value === userId;
+				});
+				next(null, punishments);
+			}
+		], (err, punishments) => {
+			if (err && err !== true) return cb(err);
+
+			cb(null, punishments);
+		});
+	},
+
+	addPunishment: function(type, value, reason, expiresAt, punishedBy, cb) {
+		async.waterfall([
+			(next) => {
+				const punishment = new db.models.punishment({
+					type,
+					value,
+					reason,
+					active: true,
+					expiresAt,
+					punishedAt: Date.now(),
+					punishedBy
+				});
+				punishment.save((err, punishment) => {
+					console.log(err);
+					if (err) return next(err);
+					next(null, punishment);
+				});
+			},
+
+			(punishment, next) => {
+				cache.hset('punishments', punishment._id, {type, value, expiresAt}, next);
+			},
+
+			(punishment, next) => {
+				// DISCORD MESSAGE
+				next();
+			}
+		], (err) => {
+			cb(err);
+		});
+	},
+
+	removePunishmentFromCache: function(punishmentId, cb) {
+		async.waterfall([
+			(next) => {
+				const punishment = new db.models.punishment({
+					type,
+					value,
+					reason,
+					active: true,
+					expiresAt,
+					punishedAt: Date.now(),
+					punishedBy
+				});
+				punishment.save((err, punishment) => {
+					console.log(err);
+					if (err) return next(err);
+					next(null, punishment);
+				});
+			},
+
+			(punishment, next) => {
+				cache.hset('punishments', punishment._id, punishment, next);
+			},
+
+			(punishment, next) => {
+				// DISCORD MESSAGE
+				next();
+			}
+		], (err) => {
+			cb(err);
+		});
+	}
+};

+ 7 - 1
frontend/App.vue

@@ -1,5 +1,5 @@
 <template>
-	<div>
+	<div v-if="!banned">
 		<h1 v-if="!socketConnected" class="alert">Could not connect to the server.</h1>
 		<router-view></router-view>
 		<toast></toast>
@@ -7,6 +7,7 @@
 		<login-modal v-if='isLoginActive'></login-modal>
 		<register-modal v-if='isRegisterActive'></register-modal>
 	</div>
+	<h1 v-if="banned">BANNED</h1>
 </template>
 
 <script>
@@ -23,6 +24,7 @@
 		replace: false,
 		data() {
 			return {
+				banned: false,
 				register: {
 					email: '',
 					username: '',
@@ -58,6 +60,10 @@
 		},
 		ready: function () {
 			let _this = this;
+			auth.isBanned((banned) => {
+				console.log("BANNED: ", banned);
+				_this.banned = banned;
+			});
 			auth.getStatus((authenticated, role, username, userId) => {
 				_this.socket = window.socket;
 				_this.loggedIn = authenticated;

+ 18 - 0
frontend/auth.js

@@ -1,4 +1,5 @@
 let callbacks = [];
+let bannedCallbacks = [];
 
 export default {
 
@@ -7,12 +8,26 @@ export default {
 	username: '',
 	userId: '',
 	role: 'default',
+	banned: null,
 
 	getStatus: function (cb) {
 		if (this.ready) cb(this.authenticated, this.role, this.username, this.userId);
 		else callbacks.push(cb);
 	},
 
+	setBanned: function() {
+		this.banned = true;
+		bannedCallbacks.forEach(callback => {
+			callback(true);
+		});
+	},
+
+	isBanned: function(cb) {
+		if (this.ready) return cb(false);
+		if (!this.ready && this.banned === true) return cb(true);
+		bannedCallbacks.push(cb);
+	},
+
 	data: function (authenticated, role, username, userId) {
 		this.authenticated = authenticated;
 		this.role = role;
@@ -22,6 +37,9 @@ export default {
 		callbacks.forEach(callback => {
 			callback(authenticated, role, username, userId);
 		});
+		bannedCallbacks.forEach(callback => {
+			callback(false);
+		});
 		callbacks = [];
 	}
 }

+ 96 - 0
frontend/components/Admin/Punishments.vue

@@ -0,0 +1,96 @@
+<template>
+	<div class='container'>
+		<table class='table is-striped'>
+			<thead>
+			<tr>
+				<td>Type</td>
+				<td>Value</td>
+				<td>Reason</td>
+				<td>Active</td>
+			</tr>
+			</thead>
+			<tbody>
+			<tr v-for='(index, punishment) in punishments' track-by='$index'>
+				<td>{{ punishment.type }}</td>
+				<td>{{ punishment.value }}</td>
+				<td>{{ punishment.reason }}</td>
+				<td>{{ (punishment.active && new Date(punishment.expiresAt).getTime() > Date.now()) ? 'Active' : 'Inactive' }}</td>
+				<td>
+					<button class='button is-primary' @click='view(punishment)'>View</button>
+				</td>
+			</tr>
+			</tbody>
+		</table>
+		<div class='card is-fullwidth'>
+			<header class='card-header'>
+				<p class='card-header-title'>Ban an IP</p>
+			</header>
+			<div class='card-content'>
+				<div class='content'>
+					<label class='label'>IP</label>
+					<p class='control is-expanded'>
+						<input class='input' type='text' placeholder='IP address (xxx.xxx.xxx.xxx)' v-model='ipBan.ip'>
+					</p>
+					<label class='label'>Reason</label>
+					<p class='control is-expanded'>
+						<input class='input' type='text' placeholder='Reason' v-model='ipBan.reason'>
+					</p>
+				</div>
+			</div>
+			<footer class='card-footer'>
+				<a class='card-footer-item' @click='banIP()' href='#'>Ban IP</a>
+			</footer>
+		</div>
+	</div>
+	<view-punishment v-show='modals.viewPunishment'></view-punishment>
+</template>
+
+<script>
+	import ViewPunishment from '../Modals/ViewPunishment.vue';
+	import { Toast } from 'vue-roaster';
+	import io from '../../io';
+
+	export default {
+		components: { ViewPunishment },
+		data() {
+			return {
+				punishments: [],
+				modals: { viewPunishment: false },
+				ipBan: {}
+			}
+		},
+		methods: {
+			toggleModal: function () {
+				this.modals.viewPunishment = !this.modals.viewPunishment;
+			},
+			view: function (punishment) {
+				this.$broadcast('viewPunishment', punishment);
+			},
+			banIP: function() {
+				let _this = this;
+				_this.socket.emit('punishments.banIP', res => {
+					Toast.methods.addToast(res.message, 6000);
+				});
+			},
+			init: function () {
+				let _this = this;
+				_this.socket.emit('punishments.index', result => {
+					if (result.status === 'success') _this.punishments = result.data;
+				});
+				//_this.socket.emit('apis.joinAdminRoom', 'punishments', () => {});
+			}
+		},
+		ready: function () {
+			let _this = this;
+			io.getSocket(socket => {
+				_this.socket = socket;
+				if (_this.socket.connected) _this.init();
+				io.onConnect(() => _this.init());
+			});
+		}
+	}
+</script>
+
+<style lang='scss' scoped>
+	body { font-family: 'Roboto', sans-serif; }
+</style>

+ 16 - 1
frontend/components/Modals/EditUser.vue

@@ -19,6 +19,11 @@
 					</span>
 					<a class="button is-info" @click='updateRole()'>Update Role</a>
 				</p>
+				<hr>
+				<p class="control has-addons">
+					<input class='input is-expanded' type='text' placeholder='Ban reason' v-model='ban.reason' autofocus>
+					<a class="button is-error" @click='banUser()'>Ban user</a>
+				</p>
 			</div>
 			<div slot='footer'>
 				<!--button class='button is-warning'>
@@ -45,7 +50,8 @@
 		components: { Modal },
 		data() {
 			return {
-				editing: {}
+				editing: {},
+				ban: {}
 			}
 		},
 		methods: {
@@ -78,6 +84,15 @@
 							this.editing._id === this.$parent.$parent.$parent.userId
 					) location.reload();
 				});
+			},
+			banUser: function() {
+				const reason = this.ban.reason;
+				if (!validation.isLength(reason, 1, 64)) return Toast.methods.addToast('Reason must have between 1 and 64 characters.', 8000);
+				if (!validation.regex.ascii.test(reason)) return Toast.methods.addToast('Invalid reason format. Only ascii characters are allowed.', 8000);
+
+				this.socket.emit(`users.banUserById`, this.editing._id, this.ban.reason, '1h', res => {
+					Toast.methods.addToast(res.message, 4000);
+				});
 			}
 		},
 		ready: function () {

+ 64 - 0
frontend/components/Modals/ViewPunishment.vue

@@ -0,0 +1,64 @@
+<template>
+	<div>
+		<modal title='View Punishment'>
+			<div slot='body'>
+				<article class="message">
+					<div class="message-body">
+						<strong>Type: </strong>{{ punishment.type }}<br/>
+						<strong>Value: </strong>{{ punishment.value }}<br/>
+						<strong>Reason: </strong>{{ punishment.reason }}<br/>
+						<strong>Active: </strong>{{ punishment.active }}<br/>
+						<strong>Expires at: </strong>{{ moment(punishment.expiresAt).format('MMMM Do YYYY, h:mm:ss a'); }} ({{ moment(punishment.expiresAt).fromNow() }})<br/>
+						<strong>Punished at: </strong>{{ moment(punishment.punishedAt).format('MMMM Do YYYY, h:mm:ss a') }} ({{ moment(punishment.punishedAt).fromNow() }})<br/>
+						<strong>Punished by: </strong>{{ punishment.punishedBy }}<br/>
+					</div>
+				</article>
+			</div>
+			<div slot='footer'>
+				<button class='button is-danger' @click='$parent.toggleModal()'>
+					<span>&nbsp;Close</span>
+				</button>
+			</div>
+		</modal>
+	</div>
+</template>
+
+<script>
+	import io from '../../io';
+	import { Toast } from 'vue-roaster';
+	import Modal from './Modal.vue';
+	import validation from '../../validation';
+
+	export default {
+		components: { Modal },
+		data() {
+			return {
+				punishment: {},
+				ban: {},
+				moment
+			}
+		},
+		methods: {},
+		ready: function () {
+			let _this = this;
+			io.getSocket(socket => _this.socket = socket );
+		},
+		events: {
+			closeModal: function () {
+				this.$parent.modals.viewPunishment = false;
+			},
+			viewPunishment: function (punishment) {
+				this.punishment = {
+					type: punishment.type,
+					value: punishment.value,
+					reason: punishment.reason,
+					active: punishment.active,
+					expiresAt: punishment.expiresAt,
+					punishedAt: punishment.punishedAt,
+					punishedBy: punishment.punishedBy
+				};
+				this.$parent.toggleModal();
+			}
+		}
+	}
+</script>

+ 13 - 1
frontend/components/pages/Admin.vue

@@ -45,6 +45,12 @@
 						<span>&nbsp;Statistics</span>
 					</a>
 				</li>
+				<li :class='{ "is-active": currentTab == "punishments" }' @click='showTab("punishments")'>
+					<a v-link="{ path: '/admin/punishments' }">
+						<i class="material-icons">gavel</i>
+						<span>&nbsp;Punishments</span>
+					</a>
+				</li>
 			</ul>
 		</div>
 
@@ -55,6 +61,7 @@
 		<news v-if='currentTab == "news"'></news>
 		<users v-if='currentTab == "users"'></users>
 		<statistics v-if='currentTab == "statistics"'></statistics>
+		<punishments v-if='currentTab == "punishments"'></punishments>
 	</div>
 </template>
 
@@ -69,6 +76,7 @@
 	import News from '../Admin/News.vue';
 	import Users from '../Admin/Users.vue';
 	import Statistics from '../Admin/Statistics.vue';
+	import Punishments from '../Admin/Punishments.vue';
 
 	export default {
 		components: {
@@ -80,7 +88,8 @@
 			Reports,
 			News,
 			Users,
-			Statistics
+			Statistics,
+			Punishments
 		},
 		ready() {
 			switch(window.location.pathname) {
@@ -105,6 +114,9 @@
 				case '/admin/statistics':
 					this.currentTab = 'statistics';
 					break;
+				case '/admin/punishments':
+					this.currentTab = 'punishments';
+					break;
 				default:
 					this.currentTab = 'queueSongs';
 			}

+ 1 - 1
frontend/io.js

@@ -49,7 +49,7 @@ export default {
 
 	removeAllListeners: function () {
 		Object.keys(this.socket._callbacks).forEach((id) => {
-			if (id.indexOf("$event:") !== -1) {
+			if (id.indexOf("$event:") !== -1 && id.indexOf("$event:keep.") === -1) {
 				delete this.socket._callbacks[id];
 			}
 		});

+ 4 - 0
frontend/main.js

@@ -35,6 +35,10 @@ lofig.get('serverDomain', function(res) {
 		socket.on("ready", (status, role, username, userId) => {
 			auth.data(status, role, username, userId);
 		});
+		socket.on("keep.me.isBanned", () => {
+			console.log("BANNED");
+			auth.setBanned();
+		});
 	});
 });