Browse Source

Added user management functionality for Admins

theflametrooper 8 years ago
parent
commit
75df701903

+ 1 - 1
backend/logic/actions/apis.js

@@ -44,7 +44,7 @@ module.exports = {
 	},
 
 	joinAdminRoom: hooks.adminRequired((session, page, cb) => {
-		if (page === 'queue' || page === 'songs' || page === 'stations' || page === 'reports' || page === 'news') {
+		if (page === 'queue' || page === 'songs' || page === 'stations' || page === 'reports' || page === 'news' || page === 'users') {
 			utils.socketJoinRoom(session.socketId, `admin.${page}`);
 		}
 		cb({});

+ 58 - 14
backend/logic/actions/users.js

@@ -23,6 +23,48 @@ cache.sub('user.updateUsername', user => {
 
 module.exports = {
 
+	/**
+	 * Lists all Users
+	 *
+	 * @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.user.find({}).exec(next);
+			}
+		], (err, users) => {
+			if (err) {
+				logger.error("USER_INDEX", `Indexing users failed. "${err.message}"`);
+				return cb({status: 'failure', message: 'Something went wrong.'});
+			} else {
+				logger.success("USER_INDEX", `Indexing users successful.`);
+				let filteredUsers = [];
+				users.forEach(user => {
+					filteredUsers.push({
+						_id: user._id,
+						username: user.username,
+						role: user.role,
+						liked: user.liked,
+						disliked: user.disliked,
+						songsRequested: user.statistics.songsRequested,
+						email: {
+							address: user.email.address,
+							verified: user.email.verified
+						},
+						hasPassword: () => {
+							if (user.services.password) return true;
+							else return false;
+						},
+						services: { github: user.services.github }
+					});
+				});
+				return cb({ status: 'success', data: filteredUsers });
+			}
+		});
+	}),
+
 	/**
 	 * Logs user in
 	 *
@@ -311,14 +353,15 @@ module.exports = {
 	 * Updates a user's username
 	 *
 	 * @param {Object} session - the session object automatically added by socket.io
+	 * @param {String} updatingUserId - the updating user's id
 	 * @param {String} newUsername - the new username
 	 * @param {Function} cb - gets called with the result
 	 * @param {String} userId - the userId automatically added by hooks
 	 */
-	updateUsername: hooks.loginRequired((session, newUsername, cb, userId) => {
+	updateUsername: hooks.loginRequired((session, updatingUserId, newUsername, cb, userId) => {
 		async.waterfall([
 			(next) => {
-				db.models.user.findOne({ _id: userId }, next);
+				db.models.user.findOne({ _id: updatingUserId }, next);
 			},
 
 			(user, next) => {
@@ -333,26 +376,26 @@ module.exports = {
 
 			(user, next) => {
 				if (!user) return next();
-				if (user._id === userId) return next();
+				if (user._id === updatingUserId) return next();
 				next('That username is already in use.');
 			},
 
 			(next) => {
-				db.models.user.update({ _id: userId }, {$set: {username: newUsername}}, next);
+				db.models.user.update({ _id: updatingUserId }, {$set: {username: newUsername}}, next);
 			}
 		], (err) => {
 			if (err && err !== true) {
 				let error = 'An error occurred.';
 				if (typeof err === "string") error = err;
 				else if (err.message) error = err.message;
-				logger.error("UPDATE_USERNAME", `Couldn't update username for user '${userId}' to username '${newUsername}'. '${error}'`);
+				logger.error("UPDATE_USERNAME", `Couldn't update username for user '${updatingUserId}' to username '${newUsername}'. '${error}'`);
 				cb({status: 'failure', message: error});
 			} else {
 				cache.pub('user.updateUsername', {
 					username: newUsername,
-					_id: userId
+					_id: updatingUserId
 				});
-				logger.success("UPDATE_USERNAME", `Updated username for user '${userId}' to username '${newUsername}'.`);
+				logger.success("UPDATE_USERNAME", `Updated username for user '${updatingUserId}' to username '${newUsername}'.`);
 				cb({ status: 'success', message: 'Username updated successfully' });
 			}
 		});
@@ -362,16 +405,17 @@ module.exports = {
 	 * Updates a user's email
 	 *
 	 * @param {Object} session - the session object automatically added by socket.io
+	 * @param {String} updatingUserId - the updating user's id
 	 * @param {String} newEmail - the new email
 	 * @param {Function} cb - gets called with the result
 	 * @param {String} userId - the userId automatically added by hooks
 	 */
-	updateEmail: hooks.loginRequired((session, newEmail, cb, userId) => {
+	updateEmail: hooks.loginRequired((session, updatingUserId, newEmail, cb, userId) => {
 		newEmail = newEmail.toLowerCase();
 		let verificationToken = utils.generateRandomString(64);
 		async.waterfall([
 			(next) => {
-				db.models.user.findOne({ _id: userId }, next);
+				db.models.user.findOne({ _id: updatingUserId }, next);
 			},
 
 			(user, next) => {
@@ -386,16 +430,16 @@ module.exports = {
 
 			(user, next) => {
 				if (!user) return next();
-				if (user._id === userId) return next();
+				if (user._id === updatingUserId) return next();
 				next('That email is already in use.');
 			},
 
 			(next) => {
-				db.models.user.update({_id: userId}, {$set: {"email.address": newEmail, "email.verified": false, "email.verificationToken": verificationToken}}, next);
+				db.models.user.update({_id: updatingUserId}, {$set: {"email.address": newEmail, "email.verified": false, "email.verificationToken": verificationToken}}, next);
 			},
 
 			(res, next) => {
-				db.models.user.findOne({ _id: userId }, next);
+				db.models.user.findOne({ _id: updatingUserId }, next);
 			},
 
 			(user, next) => {
@@ -406,10 +450,10 @@ module.exports = {
 				let error = 'An error occurred.';
 				if (typeof err === "string") error = err;
 				else if (err.message) error = err.message;
-				logger.error("UPDATE_EMAIL", `Couldn't update email for user '${userId}' to email '${newEmail}'. '${error}'`);
+				logger.error("UPDATE_EMAIL", `Couldn't update email for user '${updatingUserId}' to email '${newEmail}'. '${error}'`);
 				cb({status: 'failure', message: error});
 			} else {
-				logger.success("UPDATE_EMAIL", `Updated email for user '${userId}' to email '${newEmail}'.`);
+				logger.success("UPDATE_EMAIL", `Updated email for user '${updatingUserId}' to email '${newEmail}'.`);
 				cb({ status: 'success', message: 'Email updated successfully.' });
 			}
 		});

+ 100 - 0
frontend/components/Admin/Users.vue

@@ -0,0 +1,100 @@
+<template>
+	<div class='container'>
+		<table class='table is-striped'>
+			<thead>
+			<tr>
+				<td>Profile Picture</td>
+				<td>User ID</td>
+				<td>GitHub ID</td>
+				<td>Password</td>
+				<td>Username</td>
+				<td>Role</td>
+				<td>Email Address</td>
+				<td>Email Verified</td>
+				<td>Likes</td>
+				<td>Dislikes</td>
+				<td>Songs Requested</td>
+				<td>Options</td>
+			</tr>
+			</thead>
+			<tbody>
+			<tr v-for='(index, user) in users' track-by='$index'>
+				<td>
+					<img class='user-avatar' src='/assets/notes-transparent.png'>
+				</td>
+				<td>{{ user._id }}</td>
+				<td v-if='user.services.github'>{{ user.services.github.id }}</td>
+				<td v-else>Not Linked</td>
+				<td v-if='user.hasPassword'>Yes</td>
+				<td v-else>Not Linked</td>
+				<td>{{ user.username }}</td>
+				<td>{{ user.role }}</td>
+				<td>{{ user.email.address }}</td>
+				<td>{{ user.email.verified }}</td>
+				<td>{{ user.liked.length }}</td>
+				<td>{{ user.disliked.length }}</td>
+				<td>{{ user.songsRequested }}</td>
+				<td>
+					<button class='button is-primary' @click='edit(user)'>Edit</button>
+				</td>
+			</tr>
+			</tbody>
+		</table>
+	</div>
+	<edit-user v-show='modals.editUser'></edit-user>
+</template>
+
+<script>
+	import EditUser from '../Modals/EditUser.vue';
+	import io from '../../io';
+
+	export default {
+		components: { EditUser },
+		data() {
+			return {
+				users: [],
+				modals: { editUser: false }
+			}
+		},
+		methods: {
+			toggleModal: function () {
+				this.modals.editUser = !this.modals.editUser;
+			},
+			edit: function (user) {
+				this.$broadcast('editUser', user);
+			},
+			init: function () {
+				let _this = this;
+				_this.socket.emit('users.index', result => {
+					if (result.status === 'success') _this.users = result.data;
+				});
+				_this.socket.emit('apis.joinAdminRoom', 'users', () => {});
+				_this.socket.on('event:user.username.changed', username => {
+					_this.$parent.$parent.username = username;
+				});
+			}
+		},
+		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; }
+
+	.user-avatar {
+		display: block;
+		max-width: 50px;
+		margin: 0 auto;
+	}
+
+	td { vertical-align: middle; }
+
+	.is-primary:focus { background-color: #029ce3 !important; }
+</style>

+ 94 - 0
frontend/components/Modals/EditUser.vue

@@ -0,0 +1,94 @@
+<template>
+	<div>
+		<modal title='Edit User'>
+			<div slot='body'>
+				<p class="control has-addons">
+					<input class='input is-expanded' type='text' placeholder='Username' v-model='editing.username' autofocus>
+					<a class="button is-info" @click='updateUsername()'>Update Username</a>
+				</p>
+				<p class="control has-addons">
+					<input class='input is-expanded' type='text' placeholder='Username' v-model='editing.email.address' autofocus>
+					<a class="button is-info" @click='updateEmail()'>Update Email Address</a>
+				</p>
+				<p class="control has-addons">
+					<span class="select">
+						<select v-model="editing.role">
+							<option>default</option>
+							<option>admin</option>
+						</select>
+					</span>
+					<a class="button is-info" @click='updateRole()'>Update Role</a>
+				</p>
+			</div>
+			<div slot='footer'>
+				<button class='button is-warning'>
+					<span>&nbsp;Send Verification Email</span>
+				</button>
+				<button class='button is-warning'>
+					<span>&nbsp;Send Password Reset Email</span>
+				</button>
+				<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';
+
+	export default {
+		components: { Modal },
+		data() {
+			return {
+				editing: {},
+				video: {
+					player: null,
+					paused: false,
+					playerReady: false
+				}
+			}
+		},
+		methods: {
+			updateUsername: function () {
+				this.socket.emit(`users.updateUsername`, this.editing._id, this.editing.username, res => {
+					Toast.methods.addToast(res.message, 4000);
+				});
+			},
+			updateEmail: function () {
+				this.socket.emit(`users.updateEmail`, this.editing._id, this.editing.email.address, res => {
+					Toast.methods.addToast(res.message, 4000);
+				});
+			},
+			updateRole: function () {
+				let _this = this;
+				this.socket.emit(`users.updateRole`, this.editing._id, this.editing.role, res => {
+					Toast.methods.addToast(res.message, 4000);
+					if (res.status === 'success' && _this.editing.role === 'default') location.reload();
+				});
+			}
+		},
+		ready: function () {
+			let _this = this;
+			io.getSocket(socket => _this.socket = socket );
+		},
+		events: {
+			closeModal: function () {
+				this.$parent.modals.editUser = false;
+			},
+			editUser: function (user) {
+				this.editing = user;
+				this.$parent.toggleModal();
+			}
+		}
+	}
+</script>
+
+<style type='scss' scoped>
+	.save-changes { color: #fff; }
+
+	.tag:not(:last-child) { margin-right: 5px; }
+</style>

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

@@ -33,6 +33,12 @@
 						<span>&nbsp;News</span>
 					</a>
 				</li>
+				<li :class='{ "is-active": currentTab == "users" }' @click='showTab("users")'>
+					<a>
+						<i class="material-icons">person</i>
+						<span>&nbsp;Users</span>
+					</a>
+				</li>
 			</ul>
 		</div>
 
@@ -41,6 +47,7 @@
 		<stations v-if='currentTab == "stations"'></stations>
 		<reports v-if='currentTab == "reports"'></reports>
 		<news v-if='currentTab == "news"'></news>
+		<users v-if='currentTab == "users"'></users>
 	</div>
 </template>
 
@@ -53,6 +60,7 @@
 	import Stations from '../Admin/Stations.vue';
 	import Reports from '../Admin/Reports.vue';
 	import News from '../Admin/News.vue';
+	import Users from '../Admin/Users.vue';
 
 	export default {
 		components: {
@@ -62,7 +70,8 @@
 			Songs,
 			Stations,
 			Reports,
-			News
+			News,
+			Users
 		},
 		data() {
 			return {