Prechádzať zdrojové kódy

feat(EditUser): Added ability for admins to remove users

Owen Diffey 3 rokov pred
rodič
commit
263a15309a

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

@@ -129,7 +129,8 @@ export default {
 			room.startsWith("profile.") ||
 			room.startsWith("manage-station.") ||
 			room.startsWith("edit-song.") ||
-			room.startsWith("view-report.")
+			room.startsWith("view-report.") ||
+			room.startsWith("edit-user.")
 		) {
 			WSModule.runJob("SOCKET_JOIN_ROOM", {
 				socketId: session.socketId,

+ 179 - 0
backend/logic/actions/users.js

@@ -157,6 +157,16 @@ CacheModule.runJob("SUB", {
 	}
 });
 
+CacheModule.runJob("SUB", {
+	channel: "user.removeAccount",
+	cb: userId => {
+		WSModule.runJob("EMIT_TO_ROOMS", {
+			rooms: ["admin.users", `edit-user.${userId}`],
+			args: ["event:user.removed", { data: { userId } }]
+		});
+	}
+});
+
 export default {
 	/**
 	 * Lists all Users
@@ -360,6 +370,175 @@ export default {
 					`Successfully removed data and account for user "${session.userId}"`
 				);
 
+				CacheModule.runJob("PUB", {
+					channel: "user.removeAccount",
+					value: session.userId
+				});
+
+				return cb({
+					status: "success",
+					message: "Successfully removed data and account."
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Removes all data held on a user, including their ability to login, by userId
+	 *
+	 * @param {object} session - the session object automatically added by the websocket
+	 * @param {string} userId - the user id that is going to be banned
+	 * @param {Function} cb - gets called with the result
+	 */
+	adminRemove: isAdminRequired(async function adminRemove(session, userId, cb) {
+		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
+		const dataRequestModel = await DBModule.runJob("GET_MODEL", { modelName: "dataRequest" }, this);
+		const stationModel = await DBModule.runJob("GET_MODEL", { modelName: "station" }, this);
+		const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
+		const activityModel = await DBModule.runJob("GET_MODEL", { modelName: "activity" }, this);
+
+		const dataRequestEmail = await MailModule.runJob("GET_SCHEMA", { schemaName: "dataRequest" }, this);
+
+		const songsToAdjustRatings = [];
+
+		async.waterfall(
+			[
+				next => {
+					if (!userId) return next("You must provide a userId to remove.");
+					return next();
+				},
+				// activities related to the user
+				next => {
+					activityModel.deleteMany({ userId }, next);
+				},
+
+				// user's stations
+				(res, next) => {
+					stationModel.find({ owner: userId }, (err, stations) => {
+						if (err) return next(err);
+
+						return async.each(
+							stations,
+							(station, callback) => {
+								// delete the station
+								stationModel.deleteOne({ _id: station._id }, err => {
+									if (err) return callback(err);
+
+									// if applicable, delete the corresponding playlist for the station
+									if (station.playlist)
+										return PlaylistsModule.runJob("DELETE_PLAYLIST", {
+											playlistId: station.playlist
+										})
+											.then(() => callback())
+											.catch(callback);
+
+									return callback();
+								});
+							},
+							err => next(err)
+						);
+					});
+				},
+
+				next => {
+					playlistModel.findOne({ createdBy: userId, displayName: "Liked Songs" }, next);
+				},
+
+				// get all liked songs (as the global rating values for these songs will need adjusted)
+				(playlist, next) => {
+					if (!playlist) return next();
+
+					playlist.songs.forEach(song =>
+						songsToAdjustRatings.push({ songId: song._id, youtubeId: song.youtubeId })
+					);
+
+					return next();
+				},
+
+				next => {
+					playlistModel.findOne({ createdBy: userId, displayName: "Disliked Songs" }, next);
+				},
+
+				// get all disliked songs (as the global rating values for these songs will need adjusted)
+				(playlist, next) => {
+					if (!playlist) return next();
+
+					playlist.songs.forEach(song =>
+						songsToAdjustRatings.push({ songId: song._id, youtubeId: song.youtubeId })
+					);
+
+					return next();
+				},
+
+				// user's playlists
+				next => {
+					playlistModel.deleteMany({ createdBy: userId }, next);
+				},
+
+				(res, next) => {
+					async.each(
+						songsToAdjustRatings,
+						(song, next) => {
+							const { songId, youtubeId } = song;
+
+							SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId, youtubeId })
+								.then(() => next())
+								.catch(next);
+						},
+						err => next(err)
+					);
+				},
+
+				// user object
+				next => {
+					userModel.deleteMany({ _id: userId }, next);
+				},
+
+				// request data removal for user
+				(res, next) => {
+					dataRequestModel.create({ userId, type: "remove" }, next);
+				},
+
+				(request, next) => {
+					WSModule.runJob("EMIT_TO_ROOM", {
+						room: "admin.users",
+						args: ["event:admin.dataRequests.created", { data: { request } }]
+					});
+
+					return next();
+				},
+
+				next => userModel.find({ role: "admin" }, next),
+
+				// send email to all admins of a data removal request
+				(users, next) => {
+					if (!config.get("sendDataRequestEmails")) return next();
+					if (users.length === 0) return next();
+
+					const to = [];
+					users.forEach(user => to.push(user.email.address));
+
+					return dataRequestEmail(to, userId, "remove", err => next(err));
+				}
+			],
+			async err => {
+				if (err && err !== true) {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+					this.log(
+						"ERROR",
+						"USER_ADMIN_REMOVE",
+						`Removing data and account for user "${userId}" failed. "${err}"`
+					);
+					return cb({ status: "error", message: err });
+				}
+
+				this.log("SUCCESS", "USER_ADMIN_REMOVE", `Successfully removed data and account for user "${userId}"`);
+
+				CacheModule.runJob("PUB", {
+					channel: "user.removeAccount",
+					value: userId
+				});
+
 				return cb({
 					status: "success",
 					message: "Successfully removed data and account."

+ 34 - 4
frontend/src/components/modals/EditUser.vue

@@ -76,9 +76,12 @@
 				</div>
 			</template>
 			<template #footer>
-				<button class="button is-warning" @click="removeSessions()">
-					<span>&nbsp;Remove all sessions</span>
-				</button>
+				<confirm @confirm="removeSessions()">
+					<a class="button is-warning"> Remove all sessions </a>
+				</confirm>
+				<confirm @confirm="removeAccount()">
+					<a class="button is-danger"> Remove account </a>
+				</confirm>
 			</template>
 		</modal>
 	</div>
@@ -91,9 +94,10 @@ import Toast from "toasters";
 import validation from "@/validation";
 import ws from "@/ws";
 import Modal from "../Modal.vue";
+import Confirm from "@/components/Confirm.vue";
 
 export default {
-	components: { Modal },
+	components: { Modal, Confirm },
 	props: {
 		userId: { type: String, default: "" },
 		sector: { type: String, default: "admin" }
@@ -116,12 +120,33 @@ export default {
 	mounted() {
 		ws.onConnect(this.init);
 	},
+	beforeUnmount() {
+		this.socket.dispatch(
+			"apis.leaveRoom",
+			`edit-user.${this.userId}`,
+			() => {}
+		);
+	},
 	methods: {
 		init() {
 			this.socket.dispatch(`users.getUserFromId`, this.userId, res => {
 				if (res.status === "success") {
 					const user = res.data;
 					this.editUser(user);
+
+					this.socket.dispatch(
+						"apis.joinRoom",
+						`edit-user.${this.userId}`
+					);
+
+					this.socket.on(
+						"event:user.removed",
+						res => {
+							if (res.data.userId === this.userId)
+								this.closeModal("editUser");
+						},
+						{ modal: "editUser" }
+					);
 				} else {
 					new Toast("User with that ID not found");
 					this.closeModal("editUser");
@@ -207,6 +232,11 @@ export default {
 				}
 			);
 		},
+		removeAccount() {
+			this.socket.dispatch(`users.adminRemove`, this.user._id, res => {
+				new Toast(res.message);
+			});
+		},
 		removeSessions() {
 			this.socket.dispatch(`users.removeSessions`, this.user._id, res => {
 				new Toast(res.message);

+ 6 - 0
frontend/src/pages/Admin/tabs/Users.vue

@@ -137,6 +137,12 @@ export default {
 				request => request._id !== res.data.dataRequestId
 			);
 		});
+
+		this.socket.on("event:user.removed", res => {
+			this.users = this.users.filter(
+				user => user._id !== res.data.userId
+			);
+		});
 	},
 	methods: {
 		edit(user) {