Переглянути джерело

Finished work on banning users and IPs

theflametrooper 7 роки тому
батько
коміт
ff88583c93

+ 94 - 5
backend/logic/actions/punishments.js

@@ -1,10 +1,20 @@
 'use strict';
 
-const 	hooks 	= require('./hooks'),
-	 	async 	= require('async'),
-	 	logger 	= require('../logger'),
-	 	utils 	= require('../utils'),
-	 	db 	= require('../db');
+const 	hooks 	    = require('./hooks'),
+	 	async 	    = require('async'),
+	 	logger 	    = require('../logger'),
+	 	utils 	    = require('../utils'),
+		cache       = require('../cache'),
+	 	db 	        = require('../db'),
+		punishments = require('../punishments');
+
+cache.sub('ip.ban', ip => {
+	utils.socketsFromIP(ip, sockets => {
+		sockets.forEach(socket => {
+			socket.emit('keep.event:banned');
+		});
+	});
+});
 
 module.exports = {
 
@@ -30,4 +40,83 @@ module.exports = {
 		});
 	}),
 
+	/**
+	 * Bans an IP address
+	 *
+	 * @param {Object} session - the session object automatically added by socket.io
+	 * @param {String} value - the ip address 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
+	 */
+	banIP: hooks.adminRequired((session, value, reason, expiresAt, cb, userId) => {
+		async.waterfall([
+			(next) => {
+				if (value === '') return next('You must provide an IP address to ban.');
+				else if (reason === '') return next('You must provide a reason for the ban.');
+				else return next();
+			},
+
+			(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);
+						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('banUserIp', value, reason, expiresAt, userId, next)
+			},
+
+			(next) => {
+				cache.pub('ip.ban', value);
+				next();
+			},
+		], (err) => {
+			if (err && err !== true) {
+				err = utils.getError(err);
+				logger.error("BAN_IP", `User ${userId} failed to ban IP address ${value} with the reason ${reason}. '${err}'`);
+				cb({ status: 'failure', message: err });
+			} else {
+				logger.success("BAN_IP", `User ${userId} has successfully banned Ip address ${value} with the reason ${reason}.`);
+				cb({
+					status: 'success',
+					message: 'Successfully banned IP address.'
+				});
+			}
+		});
+	}),
+
 };

+ 16 - 7
backend/logic/actions/users.js

@@ -31,7 +31,6 @@ cache.sub('user.removeSessions', userId => {
 });
 
 cache.sub('user.linkPassword', userId => {
-	console.log("LINK4", userId);
 	utils.socketsFromUser(userId, sockets => {
 		sockets.forEach(socket => {
 			socket.emit('event:user.linkPassword');
@@ -40,7 +39,6 @@ cache.sub('user.linkPassword', userId => {
 });
 
 cache.sub('user.linkGitHub', userId => {
-	console.log("LINK1", userId);
 	utils.socketsFromUser(userId, sockets => {
 		sockets.forEach(socket => {
 			socket.emit('event:user.linkGitHub');
@@ -49,7 +47,6 @@ cache.sub('user.linkGitHub', userId => {
 });
 
 cache.sub('user.unlinkPassword', userId => {
-	console.log("LINK2", userId);
 	utils.socketsFromUser(userId, sockets => {
 		sockets.forEach(socket => {
 			socket.emit('event:user.unlinkPassword');
@@ -58,7 +55,6 @@ cache.sub('user.unlinkPassword', userId => {
 });
 
 cache.sub('user.unlinkGitHub', userId => {
-	console.log("LINK3", userId);
 	utils.socketsFromUser(userId, sockets => {
 		sockets.forEach(socket => {
 			socket.emit('event:user.unlinkGitHub');
@@ -66,6 +62,14 @@ cache.sub('user.unlinkGitHub', userId => {
 	});
 });
 
+cache.sub('user.ban', userId => {
+	utils.socketsFromUser(userId, sockets => {
+		sockets.forEach(socket => {
+			socket.emit('keep.event:banned');
+		});
+	});
+});
+
 module.exports = {
 
 	/**
@@ -1035,13 +1039,18 @@ module.exports = {
 	 */
 	banUserById: hooks.adminRequired((session, value, reason, expiresAt, cb, userId) => {
 		async.waterfall([
+			(next) => {
+				if (value === '') return next('You must provide an IP address to ban.');
+				else if (reason === '') return next('You must provide a reason for the ban.');
+				else return next();
+			},
+
 			(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);
+						expiresAt = date.setHours(date.getHours() + 1);
 						break;
 					case '12h':
 						expiresAt = date.setHours(date.getHours() + 12);
@@ -1079,7 +1088,7 @@ module.exports = {
 			},
 
 			(next) => {
-				//TODO Emit to all users with userId as value
+				cache.pub('user.ban', value);
 				next();
 			},
 		], (err) => {

+ 1 - 1
backend/logic/cache/schemas/punishment.js

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

+ 0 - 6
backend/logic/db/schemas/user.js

@@ -24,12 +24,6 @@ module.exports = {
 			access_token: String
 		}
 	},
-	ban: {
-		banned: { type: Boolean, default: false, required: true },
-		reason: String,
-		bannedAt: Date,
-		bannedUntil: Date
-	},
 	statistics: {
 		songsRequested: { type: Number, default: 0, required: true }
 	},

+ 9 - 11
backend/logic/io.js

@@ -47,23 +47,21 @@ module.exports = {
 					punishments.getPunishments((err, punishments) => {
 						const isLoggedIn = !!(socket.session && socket.session.refreshDate);
 						const userId = (isLoggedIn) ? socket.session.userId : null;
+						let ban = 0;
 						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;
-							}
+						punishments.forEach(punishment => {
+							if (punishment.expiresAt > ban) ban = 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;
+						socket.ban = ban;
 						next();
 					});
 				}
 			], () => {
 				if (!socket.session) {
-					socket.session = {socketId: socket.id};
+					socket.session = { socketId: socket.id };
 				} else socket.session.socketId = socket.id;
 				next();
 			});
@@ -75,7 +73,7 @@ module.exports = {
 			if (socket.session.sessionId) sessionInfo = ` UserID: ${socket.session.userId}.`;
 			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.emit('keep.event:banned', socket.ban);
 				socket.disconnect(true);
 			} else {
 				logger.info('IO_CONNECTION', `User connected. IP: ${socket.ip}.${sessionInfo}`);
@@ -165,4 +163,4 @@ module.exports = {
 		lockdown = true;
 	}
 
-};
+};

+ 2 - 3
backend/logic/punishments.js

@@ -77,7 +77,7 @@ module.exports = {
 					obj.punishmentId = id;
 					punishments.push(obj);
 				}
-				punishments = punishments.filter((punishment) => {
+				punishments = punishments.filter(punishment => {
 					if (punishment.expiresAt < Date.now()) punishmentsToRemove.push(punishment);
 					return punishment.expiresAt > Date.now();
 				});
@@ -176,14 +176,13 @@ module.exports = {
 					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);
+				cache.hset('punishments', punishment._id, { type, value, reason, expiresAt }, next);
 			},
 
 			(punishment, next) => {

+ 16 - 3
backend/logic/utils.js

@@ -163,9 +163,22 @@ module.exports = {
 			async.each(Object.keys(ns.connected), (id, next) => {
 				let session = ns.connected[id].session;
 				cache.hget('sessions', session.sessionId, (err, session) => {
-					if (!err && session && session.userId === userId) {
-						sockets.push(ns.connected[id]);
-					}
+					if (!err && session && session.userId === userId) sockets.push(ns.connected[id]);
+					next();
+				});
+			}, () => {
+				cb(sockets);
+			});
+		}
+	},
+	socketsFromIP: function(ip, cb) {
+		let ns = io.io.of("/");
+		let sockets = [];
+		if (ns) {
+			async.each(Object.keys(ns.connected), (id, next) => {
+				let session = ns.connected[id].session;
+				cache.hget('sessions', session.sessionId, (err, session) => {
+					if (!err && session && ns.connected[id].ip === ip) sockets.push(ns.connected[id]);
 					next();
 				});
 			}, () => {

+ 8 - 8
frontend/App.vue

@@ -1,5 +1,6 @@
 <template>
-	<div v-if="!banned">
+	<banned v-if="banned"></banned>
+	<div v-else>
 		<h1 v-if="!socketConnected" class="alert">Could not connect to the server.</h1>
 		<router-view></router-view>
 		<toast></toast>
@@ -7,12 +8,12 @@
 		<login-modal v-if='isLoginActive'></login-modal>
 		<register-modal v-if='isRegisterActive'></register-modal>
 	</div>
-	<h1 v-if="banned">BANNED</h1>
 </template>
 
 <script>
 	import { Toast } from 'vue-roaster';
 
+	import Banned from './components/pages/Banned.vue';
 	import WhatIsNew from './components/Modals/WhatIsNew.vue';
 	import LoginModal from './components/Modals/Login.vue';
 	import RegisterModal from './components/Modals/Register.vue';
@@ -25,6 +26,7 @@
 		data() {
 			return {
 				banned: false,
+				ban: {},
 				register: {
 					email: '',
 					username: '',
@@ -64,9 +66,7 @@
 					this.currentlyGettingUsernameFrom[userId] = true;
 			        io.getSocket(socket => {
 			            socket.emit('users.getUsernameFromId', userId, (data) => {
-			                if (data.status === 'success') {
-								this.$set(`userIdMap.${userId}`, data.data);
-							}
+			                if (data.status === 'success') this.$set(`userIdMap.${userId}`, data.data);
 							this.currentlyGettingUsernameFrom[userId] = false;
 						});
 					});
@@ -79,8 +79,8 @@
 			    this.$router.go(localStorage.getItem('github_redirect'));
 			    localStorage.removeItem('github_redirect');
 			}
-			auth.isBanned((banned) => {
-				console.log("BANNED: ", banned);
+			auth.isBanned((banned, ban) => {
+				_this.ban = ban;
 				_this.banned = banned;
 			});
 			auth.getStatus((authenticated, role, username, userId) => {
@@ -179,7 +179,7 @@
 				this.$broadcast('closeModal');
 			}
 		},
-		components: { Toast, WhatIsNew, LoginModal, RegisterModal }
+		components: { Toast, WhatIsNew, LoginModal, RegisterModal, Banned }
 	}
 </script>
 

+ 9 - 6
frontend/auth.js

@@ -9,22 +9,25 @@ export default {
 	userId: '',
 	role: 'default',
 	banned: null,
+	ban: {},
 
 	getStatus: function (cb) {
 		if (this.ready) cb(this.authenticated, this.role, this.username, this.userId);
 		else callbacks.push(cb);
 	},
 
-	setBanned: function() {
-		this.banned = true;
+	setBanned: function (ban) {
+		let _this = this;
+		_this.banned = true;
+		_this.ban = ban;
 		bannedCallbacks.forEach(callback => {
-			callback(true);
+			callback(true, _this.ban);
 		});
 	},
 
-	isBanned: function(cb) {
+	isBanned: function (cb) {
 		if (this.ready) return cb(false);
-		if (!this.ready && this.banned === true) return cb(true);
+		if (!this.ready && this.banned === true) return cb(true, this.ban);
 		bannedCallbacks.push(cb);
 	},
 
@@ -42,4 +45,4 @@ export default {
 		});
 		callbacks = [];
 	}
-}
+}

+ 23 - 5
frontend/components/Admin/Punishments.vue

@@ -6,12 +6,14 @@
 				<td>Type</td>
 				<td>Value</td>
 				<td>Reason</td>
-				<td>Active</td>
+				<td>Status</td>
+				<td>Options</td>
 			</tr>
 			</thead>
 			<tbody>
-			<tr v-for='(index, punishment) in punishments' track-by='$index'>
-				<td>{{ punishment.type }}</td>
+			<tr v-for='(index, punishment) in punishments | orderBy "expiresAt" -1' track-by='$index'>
+				<td v-if='punishment.type === "banUserId"'>User ID</td>
+				<td v-else>IP Address</td>
 				<td>{{ punishment.value }}</td>
 				<td>{{ punishment.reason }}</td>
 				<td>{{ (punishment.active && new Date(punishment.expiresAt).getTime() > Date.now()) ? 'Active' : 'Inactive' }}</td>
@@ -27,6 +29,17 @@
 			</header>
 			<div class='card-content'>
 				<div class='content'>
+					<label class='label'>Expires In</label>
+					<select v-model='ipBan.expiresAt'>
+						<option value='1h'>1 Hour</option>
+						<option value='12h'>12 Hours</option>
+						<option value='1d'>1 Day</option>
+						<option value='1w'>1 Week</option>
+						<option value='1m'>1 Month</option>
+						<option value='3m'>3 Months</option>
+						<option value='6m'>6 Months</option>
+						<option value='1y'>1 Year</option>
+					</select>
 					<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'>
@@ -56,7 +69,9 @@
 			return {
 				punishments: [],
 				modals: { viewPunishment: false },
-				ipBan: {}
+				ipBan: {
+					expiresAt: '1h'
+				}
 			}
 		},
 		methods: {
@@ -68,7 +83,7 @@
 			},
 			banIP: function() {
 				let _this = this;
-				_this.socket.emit('punishments.banIP', res => {
+				_this.socket.emit('punishments.banIP', _this.ipBan.ip, _this.ipBan.reason, _this.ipBan.expiresAt, res => {
 					Toast.methods.addToast(res.message, 6000);
 				});
 			},
@@ -93,4 +108,7 @@
 
 <style lang='scss' scoped>
 	body { font-family: 'Roboto', sans-serif; }
+
+	td { vertical-align: middle; }
+	select { margin-bottom: 10px; }
 </style>

+ 14 - 2
frontend/components/Modals/EditUser.vue

@@ -21,6 +21,16 @@
 				</p>
 				<hr>
 				<p class="control has-addons">
+					<select v-model='ban.expiresAt'>
+						<option value='1h'>1 Hour</option>
+						<option value='12h'>12 Hours</option>
+						<option value='1d'>1 Day</option>
+						<option value='1w'>1 Week</option>
+						<option value='1m'>1 Month</option>
+						<option value='3m'>3 Months</option>
+						<option value='6m'>6 Months</option>
+						<option value='1y'>1 Year</option>
+					</select>
 					<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>
@@ -54,7 +64,9 @@
 		data() {
 			return {
 				editing: {},
-				ban: {}
+				ban: {
+					expiresAt: '1h'
+				}
 			}
 		},
 		methods: {
@@ -93,7 +105,7 @@
 				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 => {
+				this.socket.emit(`users.banUserById`, this.editing._id, this.ban.reason, this.ban.expiresAt, res => {
 					Toast.methods.addToast(res.message, 4000);
 				});
 			},

+ 45 - 0
frontend/components/pages/Banned.vue

@@ -0,0 +1,45 @@
+<template>
+	<div class="container">
+		<i class="material-icons">not_interested</i>
+		<h4>
+			You are banned
+			for
+			<strong>{{ moment($parent.ban.expiresAt).fromNow(true) }}</strong>
+		</h4>
+		<h5 class="reason">
+			<strong>Reason: </strong>
+			{{ $parent.ban.reason }}
+		</h5>
+	</div>
+</template>
+<script>
+	export default {
+		data() {
+	        return {
+				moment
+			}
+	    }
+	}
+</script>
+
+<style lang='scss' scoped>
+	.container {
+		display: flex;
+		justify-content: center;
+		align-items: center;
+		flex-direction: column;
+		height: 100vh;
+		max-width: 1000px;
+		padding: 0 20px;
+	}
+
+	.reason {
+		text-align: justify;
+	}
+
+	i.material-icons {
+		cursor: default;
+		font-size: 65px;
+		color: tomato;
+	}
+</style>

+ 2 - 3
frontend/main.js

@@ -35,9 +35,8 @@ 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();
+		socket.on('keep.event:banned', ban => {
+			auth.setBanned(ban);
 		});
 	});
 });