2
0
Эх сурвалжийг харах

refactor: Replaced adminRequired with hasPermission hook

Owen Diffey 2 жил өмнө
parent
commit
29d6c97e0f

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

@@ -1,6 +1,6 @@
 import async from "async";
 
-import { isLoginRequired } from "./hooks";
+import isLoginRequired from "./hooks/loginRequired";
 
 // eslint-disable-next-line
 import moduleManager from "../../index";

+ 21 - 12
backend/logic/actions/apis.js

@@ -2,7 +2,8 @@ import config from "config";
 import async from "async";
 import axios from "axios";
 
-import { isAdminRequired, isLoginRequired } from "./hooks";
+import isLoginRequired from "./hooks/loginRequired";
+import { hasPermission, useHasPermission } from "./hooks/hasPermission";
 
 // eslint-disable-next-line
 import moduleManager from "../../index";
@@ -70,7 +71,7 @@ export default {
 	 * @param query - the query
 	 * @param {Function} cb
 	 */
-	searchDiscogs: isAdminRequired(function searchDiscogs(session, query, page, cb) {
+	searchDiscogs: useHasPermission("apis.searchDiscogs", function searchDiscogs(session, query, page, cb) {
 		async.waterfall(
 			[
 				next => {
@@ -187,7 +188,7 @@ export default {
 	 * @param {string} page - the admin room to join
 	 * @param {Function} cb - callback
 	 */
-	joinAdminRoom: isAdminRequired((session, page, cb) => {
+	joinAdminRoom(session, page, cb) {
 		if (
 			page === "songs" ||
 			page === "stations" ||
@@ -201,16 +202,24 @@ export default {
 			page === "youtubeVideos" ||
 			page === "import"
 		) {
-			WSModule.runJob("SOCKET_LEAVE_ROOMS", { socketId: session.socketId }).then(() => {
-				WSModule.runJob("SOCKET_JOIN_ROOM", {
-					socketId: session.socketId,
-					room: `admin.${page}`
-				});
-			});
+			hasPermission(`apis.joinAdminRoom.${page}`, session.userId)
+				.then(hasPerm => {
+					if (hasPerm)
+						WSModule.runJob("SOCKET_LEAVE_ROOMS", { socketId: session.socketId }).then(() => {
+							WSModule.runJob(
+								"SOCKET_JOIN_ROOM",
+								{
+									socketId: session.socketId,
+									room: `admin.${page}`
+								},
+								this
+							).then(() => cb({ status: "success", message: "Successfully joined admin room." }));
+						});
+					else cb({ status: "error", message: "Failed to join admin room." });
+				})
+				.catch(() => cb({ status: "error", message: "Failed to join admin room." }));
 		}
-
-		cb({ status: "success", message: "Successfully joined admin room." });
-	}),
+	},
 
 	/**
 	 * Leaves all rooms

+ 47 - 44
backend/logic/actions/dataRequests.js

@@ -1,6 +1,6 @@
 import async from "async";
 
-import { isAdminRequired } from "./hooks";
+import { useHasPermission } from "./hooks/hasPermission";
 
 // eslint-disable-next-line
 import moduleManager from "../../index";
@@ -39,49 +39,52 @@ export default {
 	 * @param operator - the operator for queries
 	 * @param cb
 	 */
-	getData: isAdminRequired(async function getSet(session, page, pageSize, properties, sort, queries, operator, cb) {
-		async.waterfall(
-			[
-				next => {
-					DBModule.runJob(
-						"GET_DATA",
-						{
-							page,
-							pageSize,
-							properties,
-							sort,
-							queries,
-							operator,
-							modelName: "dataRequest",
-							blacklistedProperties: [],
-							specialProperties: {},
-							specialQueries: {}
-						},
-						this
-					)
-						.then(response => {
-							next(null, response);
-						})
-						.catch(err => {
-							next(err);
-						});
-				}
-			],
-			async (err, response) => {
-				if (err && err !== true) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log("ERROR", "DATA_REQUESTS_GET_DATA", `Failed to get data from data requests. "${err}"`);
-					return cb({ status: "error", message: err });
+	getData: useHasPermission(
+		"dataRequests.getData",
+		async function getData(session, page, pageSize, properties, sort, queries, operator, cb) {
+			async.waterfall(
+				[
+					next => {
+						DBModule.runJob(
+							"GET_DATA",
+							{
+								page,
+								pageSize,
+								properties,
+								sort,
+								queries,
+								operator,
+								modelName: "dataRequest",
+								blacklistedProperties: [],
+								specialProperties: {},
+								specialQueries: {}
+							},
+							this
+						)
+							.then(response => {
+								next(null, response);
+							})
+							.catch(err => {
+								next(err);
+							});
+					}
+				],
+				async (err, response) => {
+					if (err && err !== true) {
+						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+						this.log("ERROR", "DATA_REQUESTS_GET_DATA", `Failed to get data from data requests. "${err}"`);
+						return cb({ status: "error", message: err });
+					}
+					this.log("SUCCESS", "DATA_REQUESTS_GET_DATA", `Got data from data requests successfully.`);
+					return cb({
+						status: "success",
+						message: "Successfully got data from data requests.",
+						data: response
+					});
 				}
-				this.log("SUCCESS", "DATA_REQUESTS_GET_DATA", `Got data from data requests successfully.`);
-				return cb({
-					status: "success",
-					message: "Successfully got data from data requests.",
-					data: response
-				});
-			}
-		);
-	}),
+			);
+		}
+	),
 
 	/**
 	 * Resolves a data request
@@ -91,7 +94,7 @@ export default {
 	 * @param {boolean} resolved - whether to set to resolved to true or false
 	 * @param {Function} cb - gets called with the result
 	 */
-	resolve: isAdminRequired(async function resolve(session, dataRequestId, resolved, cb) {
+	resolve: useHasPermission("dataRequests.resolve", async function resolve(session, dataRequestId, resolved, cb) {
 		const dataRequestModel = await DBModule.runJob("GET_MODEL", { modelName: "dataRequest" }, this);
 
 		async.waterfall(

+ 0 - 52
backend/logic/actions/hooks/adminRequired.js

@@ -1,52 +0,0 @@
-import async from "async";
-
-// eslint-disable-next-line
-import moduleManager from "../../../index";
-
-const DBModule = moduleManager.modules.db;
-const CacheModule = moduleManager.modules.cache;
-const UtilsModule = moduleManager.modules.utils;
-
-export default destination =>
-	async function adminRequired(session, ...args) {
-		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
-
-		const cb = args[args.length - 1];
-
-		async.waterfall(
-			[
-				next => {
-					CacheModule.runJob(
-						"HGET",
-						{
-							table: "sessions",
-							key: session.sessionId
-						},
-						this
-					)
-						.then(session => {
-							next(null, session);
-						})
-						.catch(next);
-				},
-				(session, next) => {
-					if (!session || !session.userId) return next("Login required.");
-					return userModel.findOne({ _id: session.userId }, next);
-				},
-				(user, next) => {
-					if (!user) return next("Login required.");
-					if (user.role !== "admin") return next("Insufficient permissions.");
-					return next();
-				}
-			],
-			async err => {
-				if (err) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log("INFO", "ADMIN_REQUIRED", `User failed to pass admin required check. "${err}"`);
-					return cb({ status: "error", message: err });
-				}
-				this.log("INFO", "ADMIN_REQUIRED", `User "${session.userId}" passed admin required check.`, false);
-				return destination.apply(this, [session].concat(args));
-			}
-		);
-	};

+ 216 - 0
backend/logic/actions/hooks/hasPermission.js

@@ -0,0 +1,216 @@
+import async from "async";
+
+// eslint-disable-next-line
+import moduleManager from "../../../index";
+
+const DBModule = moduleManager.modules.db;
+const CacheModule = moduleManager.modules.cache;
+const UtilsModule = moduleManager.modules.utils;
+const StationsModule = moduleManager.modules.stations;
+
+const permissions = {};
+permissions.dj = {
+	"test.queue.add": true,
+	"test.queue.remove": false
+};
+permissions.owner = {
+	...permissions.dj,
+	"test.queue.remove": true
+};
+permissions.moderator = {
+	...permissions.owner,
+	"test.remove.other": false,
+	"songs.length": true,
+	"songs.getData": true,
+	"songs.getSongFromId": true,
+	"songs.getSongsFromYoutubeIds": true,
+	"songs.create": true,
+	"songs.update": true,
+	"songs.verify": true,
+	"songs.verifyMany": true,
+	"songs.unverify": true,
+	"songs.unverifyMany": true,
+	"songs.getGenres": true,
+	"songs.editGenres": true,
+	"songs.getArtists": true,
+	"songs.editArtists": true,
+	"songs.getTags": true,
+	"songs.editTags": true,
+	"apis.searchDiscogs": true,
+	"apis.joinAdminRoom.songs": true,
+	"apis.joinAdminRoom.stations": true,
+	"apis.joinAdminRoom.reports": true,
+	"apis.joinAdminRoom.news": true,
+	"apis.joinAdminRoom.playlists": true,
+	"apis.joinAdminRoom.punishments": true,
+	"apis.joinAdminRoom.youtubeVideos": true,
+	"apis.joinAdminRoom.import": true,
+	"media.getImportJobs": true,
+	"news.getData": true,
+	"news.create": true,
+	"news.update": true,
+	"playlists.getData": true,
+	"playlists.searchOfficial": true,
+	"playlists.updatePrivacyAdmin": true,
+	"punishments.getData": true,
+	"punishments.getPunishmentsForUser": true,
+	"punishments.findOne": true,
+	"punishments.banIP": true,
+	"reports.getData": true,
+	"reports.findOne": true,
+	"reports.getReportsForSong": true,
+	"reports.resolve": true,
+	"reports.toggleIssue": true,
+	"stations.getData": true,
+	"stations.resetQueue": true,
+	"youtube.getVideos": true,
+	"youtube.requestSetAdmin": true
+};
+permissions.admin = {
+	...permissions.moderator,
+	"test.remove.other": true,
+	"songs.updateAll": true,
+	"songs.remove": true,
+	"songs.removeMany": true,
+	"apis.joinAdminRoom.users": true,
+	"apis.joinAdminRoom.statistics": true,
+	"apis.joinAdminRoom.youtube": true,
+	"dataRequests.getData": true,
+	"dataRequests.resolve": true,
+	"media.recalculateAllRatings": true,
+	"media.removeImportJobs": true,
+	"news.remove": true,
+	"playlists.removeAdmin": true,
+	"playlists.deleteOrphanedStationPlaylists": true,
+	"playlists.deleteOrphanedGenrePlaylists": true,
+	"playlists.requestOrphanedPlaylistSongs": true,
+	"playlists.clearAndRefillStationPlaylist": true,
+	"playlists.clearAndRefillGenrePlaylist": true,
+	"playlists.clearAndRefillAllStationPlaylists": true,
+	"playlists.clearAndRefillAllGenrePlaylists": true,
+	"playlists.createMissingGenrePlaylists": true,
+	"reports.remove": true,
+	"stations.clearEveryStationQueue": true,
+	"users.getData": true,
+	"users.adminRemove": true,
+	"users.getUserFromId": true,
+	"users.updateRole": true,
+	"users.adminRequestPasswordReset": true,
+	"users.resendVerifyEmail": true,
+	"users.banUserById": true,
+	"utils.getModules": true,
+	"utils.getModule": true,
+	"youtube.getQuotaStatus": true,
+	"youtube.getQuotaChartData": true,
+	"youtube.getApiRequests": true,
+	"youtube.getApiRequest": true,
+	"youtube.resetStoredApiRequests": true,
+	"youtube.removeStoredApiRequest": true,
+	"youtube.removeVideos": true
+};
+
+export const hasPermission = async (permission, userId, stationId) => {
+	const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
+
+	return new Promise(resolve => {
+		async.waterfall(
+			[
+				next => {
+					if (!userId) return next("User ID required.");
+					return userModel.findOne({ _id: userId }, next);
+				},
+				(user, next) => {
+					if (!user) return next("Login required.");
+					if (!stationId) return next(null, [user.role]);
+					return StationsModule.runJob("GET_STATION", { stationId }, this)
+						.then(station => {
+							if (!station) return next("Station not found.");
+							if (station.type === "community" && station.owner === userId)
+								return next(null, [user.role, "owner"]);
+							// if (station.type === "community" && station.djs.find(userId))
+							// 	return next(null, [user.role, "dj"]);
+							return next("Invalid permissions.");
+						})
+						.catch(next);
+				},
+				(roles, next) => {
+					if (!roles) return next("Role required.");
+					let permissionFound;
+					roles.forEach(role => {
+						if (permissions[role] && permissions[role][permission]) permissionFound = true;
+					});
+					if (permissionFound) return next();
+					return next("Insufficient permissions.");
+				}
+			],
+			async err => {
+				if (err) {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+					// TODO
+					// this.log(
+					// 	"INFO",
+					// 	"HAS_PERMISSION",
+					// 	`User "${userId}" does not have required permission "${permission}". "${err}"`
+					// );
+					return resolve(false);
+				}
+				// TODO
+				// this.log("INFO", "HAS_PERMISSION", `User "${userId}" has required permission "${permission}".`, false);
+				return resolve(true);
+			}
+		);
+	});
+};
+
+export const useHasPermission = (options, destination) =>
+	async function useHasPermission(session, ...args) {
+		const permission = typeof options === "object" ? options.permission : options;
+		const stationId = typeof options === "object" ? options.stationId : null;
+		const cb = args[args.length - 1];
+
+		async.waterfall(
+			[
+				next => {
+					CacheModule.runJob(
+						"HGET",
+						{
+							table: "sessions",
+							key: session.sessionId
+						},
+						this
+					)
+						.then(session => {
+							next(null, session);
+						})
+						.catch(next);
+				},
+				(session, next) => {
+					if (!session || !session.userId) return next("Login required.");
+					return hasPermission(permission, session.userId, stationId)
+						.then(hasPerm => {
+							if (hasPerm) return next();
+							return next("Insufficient permissions.");
+						})
+						.catch(next);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+					this.log(
+						"INFO",
+						"USE_HAS_PERMISSION",
+						`User "${session.userId}" does not have required permission "${permission}". "${err}"`
+					);
+					return cb({ status: "error", message: err });
+				}
+				this.log(
+					"INFO",
+					"USE_HAS_PERMISSION",
+					`User "${session.userId}" has required permission "${permission}".`,
+					false
+				);
+				return destination.apply(this, [session].concat(args));
+			}
+		);
+	};

+ 0 - 7
backend/logic/actions/hooks/index.js

@@ -1,7 +0,0 @@
-import loginRequired from "./loginRequired";
-import adminRequired from "./adminRequired";
-import ownerRequired from "./ownerRequired";
-
-export const isLoginRequired = loginRequired;
-export const isAdminRequired = adminRequired;
-export const isOwnerRequired = ownerRequired;

+ 100 - 98
backend/logic/actions/media.js

@@ -1,6 +1,7 @@
 import async from "async";
 
-import { isAdminRequired, isLoginRequired } from "./hooks";
+import isLoginRequired from "./hooks/loginRequired";
+import { useHasPermission } from "./hooks/hasPermission";
 
 // eslint-disable-next-line
 import moduleManager from "../../index";
@@ -128,55 +129,62 @@ export default {
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param cb
 	 */
-	recalculateAllRatings: isAdminRequired(async function recalculateAllRatings(session, cb) {
-		this.keepLongJob();
-		this.publishProgress({
-			status: "started",
-			title: "Recalculate all ratings",
-			message: "Recalculating all ratings.",
-			id: this.toString()
-		});
-		await CacheModule.runJob("RPUSH", { key: `longJobs.${session.userId}`, value: this.toString() }, this);
-		await CacheModule.runJob(
-			"PUB",
-			{
-				channel: "longJob.added",
-				value: { jobId: this.toString(), userId: session.userId }
-			},
-			this
-		);
-
-		async.waterfall(
-			[
-				next => {
-					MediaModule.runJob("RECALCULATE_ALL_RATINGS", {}, this)
-						.then(() => {
-							next();
-						})
-						.catch(err => {
-							next(err);
+	recalculateAllRatings: useHasPermission(
+		"media.recalculateAllRatings",
+		async function recalculateAllRatings(session, cb) {
+			this.keepLongJob();
+			this.publishProgress({
+				status: "started",
+				title: "Recalculate all ratings",
+				message: "Recalculating all ratings.",
+				id: this.toString()
+			});
+			await CacheModule.runJob("RPUSH", { key: `longJobs.${session.userId}`, value: this.toString() }, this);
+			await CacheModule.runJob(
+				"PUB",
+				{
+					channel: "longJob.added",
+					value: { jobId: this.toString(), userId: session.userId }
+				},
+				this
+			);
+
+			async.waterfall(
+				[
+					next => {
+						MediaModule.runJob("RECALCULATE_ALL_RATINGS", {}, this)
+							.then(() => {
+								next();
+							})
+							.catch(err => {
+								next(err);
+							});
+					}
+				],
+				async err => {
+					if (err) {
+						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+						this.log(
+							"ERROR",
+							"MEDIA_RECALCULATE_ALL_RATINGS",
+							`Failed to recalculate all ratings. "${err}"`
+						);
+						this.publishProgress({
+							status: "error",
+							message: err
 						});
-				}
-			],
-			async err => {
-				if (err) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log("ERROR", "MEDIA_RECALCULATE_ALL_RATINGS", `Failed to recalculate all ratings. "${err}"`);
+						return cb({ status: "error", message: err });
+					}
+					this.log("SUCCESS", "MEDIA_RECALCULATE_ALL_RATINGS", `Recalculated all ratings successfully.`);
 					this.publishProgress({
-						status: "error",
-						message: err
+						status: "success",
+						message: "Successfully recalculated all ratings."
 					});
-					return cb({ status: "error", message: err });
+					return cb({ status: "success", message: "Successfully recalculated all ratings." });
 				}
-				this.log("SUCCESS", "MEDIA_RECALCULATE_ALL_RATINGS", `Recalculated all ratings successfully.`);
-				this.publishProgress({
-					status: "success",
-					message: "Successfully recalculated all ratings."
-				});
-				return cb({ status: "success", message: "Successfully recalculated all ratings." });
-			}
-		);
-	}),
+			);
+		}
+	),
 
 	/**
 	 * Like
@@ -841,65 +849,59 @@ export default {
 	 * @param operator - the operator for queries
 	 * @param cb
 	 */
-	getImportJobs: isAdminRequired(async function getImportJobs(
-		session,
-		page,
-		pageSize,
-		properties,
-		sort,
-		queries,
-		operator,
-		cb
-	) {
-		async.waterfall(
-			[
-				next => {
-					DBModule.runJob(
-						"GET_DATA",
-						{
-							page,
-							pageSize,
-							properties,
-							sort,
-							queries,
-							operator,
-							modelName: "importJob",
-							blacklistedProperties: [],
-							specialProperties: {},
-							specialQueries: {}
-						},
-						this
-					)
-						.then(response => {
-							next(null, response);
-						})
-						.catch(err => {
-							next(err);
-						});
-				}
-			],
-			async (err, response) => {
-				if (err && err !== true) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log("ERROR", "MEDIA_GET_IMPORT_JOBS", `Failed to get import jobs. "${err}"`);
-					return cb({ status: "error", message: err });
+	getImportJobs: useHasPermission(
+		"media.getImportJobs",
+		async function getImportJobs(session, page, pageSize, properties, sort, queries, operator, cb) {
+			async.waterfall(
+				[
+					next => {
+						DBModule.runJob(
+							"GET_DATA",
+							{
+								page,
+								pageSize,
+								properties,
+								sort,
+								queries,
+								operator,
+								modelName: "importJob",
+								blacklistedProperties: [],
+								specialProperties: {},
+								specialQueries: {}
+							},
+							this
+						)
+							.then(response => {
+								next(null, response);
+							})
+							.catch(err => {
+								next(err);
+							});
+					}
+				],
+				async (err, response) => {
+					if (err && err !== true) {
+						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+						this.log("ERROR", "MEDIA_GET_IMPORT_JOBS", `Failed to get import jobs. "${err}"`);
+						return cb({ status: "error", message: err });
+					}
+					this.log("SUCCESS", "MEDIA_GET_IMPORT_JOBS", `Fetched import jobs successfully.`);
+					return cb({
+						status: "success",
+						message: "Successfully fetched import jobs.",
+						data: response
+					});
 				}
-				this.log("SUCCESS", "MEDIA_GET_IMPORT_JOBS", `Fetched import jobs successfully.`);
-				return cb({
-					status: "success",
-					message: "Successfully fetched import jobs.",
-					data: response
-				});
-			}
-		);
-	}),
+			);
+		}
+	),
 
 	/**
 	 * Remove import jobs
 	 *
 	 * @returns {{status: string, data: object}}
 	 */
-	removeImportJobs: isAdminRequired(function removeImportJobs(session, jobIds, cb) {
+	removeImportJobs: useHasPermission("media.removeImportJobs", function removeImportJobs(session, jobIds, cb) {
 		MediaModule.runJob("REMOVE_IMPORT_JOBS", { jobIds }, this)
 			.then(() => {
 				this.log("SUCCESS", "MEDIA_REMOVE_IMPORT_JOBS", `Removing import jobs was successful.`);

+ 90 - 85
backend/logic/actions/news.js

@@ -1,6 +1,6 @@
 import async from "async";
 
-import { isAdminRequired } from "./hooks";
+import { useHasPermission } from "./hooks/hasPermission";
 
 // eslint-disable-next-line
 import moduleManager from "../../index";
@@ -69,93 +69,98 @@ export default {
 	 * @param operator - the operator for queries
 	 * @param cb
 	 */
-	getData: isAdminRequired(async function getSet(session, page, pageSize, properties, sort, queries, operator, cb) {
-		async.waterfall(
-			[
-				next => {
-					DBModule.runJob(
-						"GET_DATA",
-						{
-							page,
-							pageSize,
-							properties,
-							sort,
-							queries,
-							operator,
-							modelName: "news",
-							blacklistedProperties: [],
-							specialProperties: {
-								createdBy: [
-									{
-										$addFields: {
-											createdByOID: {
-												$convert: {
-													input: "$createdBy",
-													to: "objectId",
-													onError: "unknown",
-													onNull: "unknown"
+	getData: useHasPermission(
+		"news.getData",
+		async function getSet(session, page, pageSize, properties, sort, queries, operator, cb) {
+			async.waterfall(
+				[
+					next => {
+						DBModule.runJob(
+							"GET_DATA",
+							{
+								page,
+								pageSize,
+								properties,
+								sort,
+								queries,
+								operator,
+								modelName: "news",
+								blacklistedProperties: [],
+								specialProperties: {
+									createdBy: [
+										{
+											$addFields: {
+												createdByOID: {
+													$convert: {
+														input: "$createdBy",
+														to: "objectId",
+														onError: "unknown",
+														onNull: "unknown"
+													}
 												}
 											}
-										}
-									},
-									{
-										$lookup: {
-											from: "users",
-											localField: "createdByOID",
-											foreignField: "_id",
-											as: "createdByUser"
-										}
-									},
-									{
-										$unwind: {
-											path: "$createdByUser",
-											preserveNullAndEmptyArrays: true
-										}
-									},
-									{
-										$addFields: {
-											createdByUsername: {
-												$ifNull: ["$createdByUser.username", "unknown"]
+										},
+										{
+											$lookup: {
+												from: "users",
+												localField: "createdByOID",
+												foreignField: "_id",
+												as: "createdByUser"
+											}
+										},
+										{
+											$unwind: {
+												path: "$createdByUser",
+												preserveNullAndEmptyArrays: true
+											}
+										},
+										{
+											$addFields: {
+												createdByUsername: {
+													$ifNull: ["$createdByUser.username", "unknown"]
+												}
+											}
+										},
+										{
+											$project: {
+												createdByOID: 0,
+												createdByUser: 0
 											}
 										}
-									},
-									{
-										$project: {
-											createdByOID: 0,
-											createdByUser: 0
-										}
-									}
-								]
+									]
+								},
+								specialQueries: {
+									createdBy: newQuery => ({
+										$or: [newQuery, { createdByUsername: newQuery.createdBy }]
+									})
+								}
 							},
-							specialQueries: {
-								createdBy: newQuery => ({ $or: [newQuery, { createdByUsername: newQuery.createdBy }] })
-							}
-						},
-						this
-					)
-						.then(response => {
-							next(null, response);
-						})
-						.catch(err => {
-							next(err);
-						});
+							this
+						)
+							.then(response => {
+								next(null, response);
+							})
+							.catch(err => {
+								next(err);
+							});
+					}
+				],
+				async (err, response) => {
+					if (err && err !== true) {
+						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+						this.log("ERROR", "NEWS_GET_DATA", `Failed to get data from news. "${err}"`);
+						return cb({ status: "error", message: err });
+					}
+					this.log("SUCCESS", "NEWS_GET_DATA", `Got data from news successfully.`);
+					return cb({
+						status: "success",
+						message: "Successfully got data from news.",
+						data: response
+					});
 				}
-			],
-			async (err, response) => {
-				if (err && err !== true) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log("ERROR", "NEWS_GET_DATA", `Failed to get data from news. "${err}"`);
-					return cb({ status: "error", message: err });
-				}
-				this.log("SUCCESS", "NEWS_GET_DATA", `Got data from news successfully.`);
-				return cb({
-					status: "success",
-					message: "Successfully got data from news.",
-					data: response
-				});
-			}
-		);
-	}),
+			);
+		}
+	),
 
 	/**
 	 * Gets all news items that are published
@@ -221,7 +226,7 @@ export default {
 	 * @param {object} data - the object of the news data
 	 * @param {Function} cb - gets called with the result
 	 */
-	create: isAdminRequired(async function create(session, data, cb) {
+	create: useHasPermission("news.create", async function create(session, data, cb) {
 		const newsModel = await DBModule.runJob("GET_MODEL", { modelName: "news" }, this);
 		async.waterfall(
 			[
@@ -283,7 +288,7 @@ export default {
 	 * @param {object} newsId - the id of the news item we want to remove
 	 * @param {Function} cb - gets called with the result
 	 */
-	remove: isAdminRequired(async function remove(session, newsId, cb) {
+	remove: useHasPermission("news.remove", async function remove(session, newsId, cb) {
 		const newsModel = await DBModule.runJob("GET_MODEL", { modelName: "news" }, this);
 
 		async.waterfall(
@@ -331,7 +336,7 @@ export default {
 	 * @param {string} item.markdown - the markdown that forms the content of the news
 	 * @param {Function} cb - gets called with the result
 	 */
-	update: isAdminRequired(async function update(session, newsId, item, cb) {
+	update: useHasPermission("news.update", async function update(session, newsId, item, cb) {
 		const newsModel = await DBModule.runJob("GET_MODEL", { modelName: "news" }, this);
 
 		async.waterfall(

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 501 - 466
backend/logic/actions/playlists.js


+ 167 - 161
backend/logic/actions/punishments.js

@@ -1,6 +1,6 @@
 import async from "async";
 
-import { isAdminRequired } from "./hooks";
+import { useHasPermission } from "./hooks/hasPermission";
 
 // eslint-disable-next-line
 import moduleManager from "../../index";
@@ -40,160 +40,163 @@ export default {
 	 * @param operator - the operator for queries
 	 * @param cb
 	 */
-	getData: isAdminRequired(async function getSet(session, page, pageSize, properties, sort, queries, operator, cb) {
-		async.waterfall(
-			[
-				next => {
-					DBModule.runJob(
-						"GET_DATA",
-						{
-							page,
-							pageSize,
-							properties,
-							sort,
-							queries,
-							operator,
-							modelName: "punishment",
-							blacklistedProperties: [],
-							specialProperties: {
-								status: [
-									{
-										$addFields: {
-											status: {
-												$cond: [
-													{ $eq: ["$active", true] },
-													{
-														$cond: [
-															{ $gt: [new Date(), "$expiresAt"] },
-															"Inactive",
-															"Active"
-														]
-													},
-													"Inactive"
-												]
+	getData: useHasPermission(
+		"punishments.getData",
+		async function getSet(session, page, pageSize, properties, sort, queries, operator, cb) {
+			async.waterfall(
+				[
+					next => {
+						DBModule.runJob(
+							"GET_DATA",
+							{
+								page,
+								pageSize,
+								properties,
+								sort,
+								queries,
+								operator,
+								modelName: "punishment",
+								blacklistedProperties: [],
+								specialProperties: {
+									status: [
+										{
+											$addFields: {
+												status: {
+													$cond: [
+														{ $eq: ["$active", true] },
+														{
+															$cond: [
+																{ $gt: [new Date(), "$expiresAt"] },
+																"Inactive",
+																"Active"
+															]
+														},
+														"Inactive"
+													]
+												}
 											}
 										}
-									}
-								],
-								value: [
-									{
-										$addFields: {
-											valueOID: {
-												$convert: {
-													input: "$value",
-													to: "objectId",
-													onError: "unknown",
-													onNull: "unknown"
+									],
+									value: [
+										{
+											$addFields: {
+												valueOID: {
+													$convert: {
+														input: "$value",
+														to: "objectId",
+														onError: "unknown",
+														onNull: "unknown"
+													}
 												}
 											}
-										}
-									},
-									{
-										$lookup: {
-											from: "users",
-											localField: "valueOID",
-											foreignField: "_id",
-											as: "valueUser"
-										}
-									},
-									{
-										$unwind: {
-											path: "$valueUser",
-											preserveNullAndEmptyArrays: true
-										}
-									},
-									{
-										$addFields: {
-											valueUsername: {
-												$cond: [
-													{ $eq: ["$type", "banUserId"] },
-													{ $ifNull: ["$valueUser.username", "unknown"] },
-													null
-												]
+										},
+										{
+											$lookup: {
+												from: "users",
+												localField: "valueOID",
+												foreignField: "_id",
+												as: "valueUser"
 											}
-										}
-									},
-									{
-										$project: {
-											valueOID: 0,
-											valueUser: 0
-										}
-									}
-								],
-								punishedBy: [
-									{
-										$addFields: {
-											punishedByOID: {
-												$convert: {
-													input: "$punishedBy",
-													to: "objectId",
-													onError: "unknown",
-													onNull: "unknown"
+										},
+										{
+											$unwind: {
+												path: "$valueUser",
+												preserveNullAndEmptyArrays: true
+											}
+										},
+										{
+											$addFields: {
+												valueUsername: {
+													$cond: [
+														{ $eq: ["$type", "banUserId"] },
+														{ $ifNull: ["$valueUser.username", "unknown"] },
+														null
+													]
 												}
 											}
-										}
-									},
-									{
-										$lookup: {
-											from: "users",
-											localField: "punishedByOID",
-											foreignField: "_id",
-											as: "punishedByUser"
-										}
-									},
-									{
-										$unwind: {
-											path: "$punishedByUser",
-											preserveNullAndEmptyArrays: true
-										}
-									},
-									{
-										$addFields: {
-											punishedByUsername: {
-												$ifNull: ["$punishedByUser.username", "unknown"]
+										},
+										{
+											$project: {
+												valueOID: 0,
+												valueUser: 0
 											}
 										}
-									},
-									{
-										$project: {
-											punishedByOID: 0,
-											punishedByUser: 0
+									],
+									punishedBy: [
+										{
+											$addFields: {
+												punishedByOID: {
+													$convert: {
+														input: "$punishedBy",
+														to: "objectId",
+														onError: "unknown",
+														onNull: "unknown"
+													}
+												}
+											}
+										},
+										{
+											$lookup: {
+												from: "users",
+												localField: "punishedByOID",
+												foreignField: "_id",
+												as: "punishedByUser"
+											}
+										},
+										{
+											$unwind: {
+												path: "$punishedByUser",
+												preserveNullAndEmptyArrays: true
+											}
+										},
+										{
+											$addFields: {
+												punishedByUsername: {
+													$ifNull: ["$punishedByUser.username", "unknown"]
+												}
+											}
+										},
+										{
+											$project: {
+												punishedByOID: 0,
+												punishedByUser: 0
+											}
 										}
-									}
-								]
+									]
+								},
+								specialQueries: {
+									value: newQuery => ({ $or: [newQuery, { valueUsername: newQuery.value }] }),
+									punishedBy: newQuery => ({
+										$or: [newQuery, { punishedByUsername: newQuery.punishedBy }]
+									})
+								}
 							},
-							specialQueries: {
-								value: newQuery => ({ $or: [newQuery, { valueUsername: newQuery.value }] }),
-								punishedBy: newQuery => ({
-									$or: [newQuery, { punishedByUsername: newQuery.punishedBy }]
-								})
-							}
-						},
-						this
-					)
-						.then(response => {
-							next(null, response);
-						})
-						.catch(err => {
-							next(err);
-						});
-				}
-			],
-			async (err, response) => {
-				if (err && err !== true) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log("ERROR", "PUNISHMENTS_GET_DATA", `Failed to get data from punishments. "${err}"`);
-					return cb({ status: "error", message: err });
+							this
+						)
+							.then(response => {
+								next(null, response);
+							})
+							.catch(err => {
+								next(err);
+							});
+					}
+				],
+				async (err, response) => {
+					if (err && err !== true) {
+						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+						this.log("ERROR", "PUNISHMENTS_GET_DATA", `Failed to get data from punishments. "${err}"`);
+						return cb({ status: "error", message: err });
+					}
+					this.log("SUCCESS", "PUNISHMENTS_GET_DATA", `Got data from punishments successfully.`);
+					return cb({
+						status: "success",
+						message: "Successfully got data from punishments.",
+						data: response
+					});
 				}
-				this.log("SUCCESS", "PUNISHMENTS_GET_DATA", `Got data from punishments successfully.`);
-				return cb({
-					status: "success",
-					message: "Successfully got data from punishments.",
-					data: response
-				});
-			}
-		);
-	}),
+			);
+		}
+	),
 
 	/**
 	 * Gets all punishments for a user
@@ -202,26 +205,29 @@ export default {
 	 * @param {string} userId - the id of the user
 	 * @param {Function} cb - gets called with the result
 	 */
-	getPunishmentsForUser: isAdminRequired(async function getPunishmentsForUser(session, userId, cb) {
-		const punishmentModel = await DBModule.runJob("GET_MODEL", { modelName: "punishment" }, this);
+	getPunishmentsForUser: useHasPermission(
+		"punishments.getPunishmentsForUser",
+		async function getPunishmentsForUser(session, userId, cb) {
+			const punishmentModel = await DBModule.runJob("GET_MODEL", { modelName: "punishment" }, this);
 
-		punishmentModel.find({ type: "banUserId", value: userId }, async (err, punishments) => {
-			if (err) {
-				err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+			punishmentModel.find({ type: "banUserId", value: userId }, async (err, punishments) => {
+				if (err) {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
 
-				this.log(
-					"ERROR",
-					"GET_PUNISHMENTS_FOR_USER",
-					`Getting punishments for user ${userId} failed. "${err}"`
-				);
+					this.log(
+						"ERROR",
+						"GET_PUNISHMENTS_FOR_USER",
+						`Getting punishments for user ${userId} failed. "${err}"`
+					);
 
-				return cb({ status: "error", message: err });
-			}
+					return cb({ status: "error", message: err });
+				}
 
-			this.log("SUCCESS", "GET_PUNISHMENTS_FOR_USER", `Got punishments for user ${userId} successful.`);
-			return cb({ status: "success", data: { punishments } });
-		});
-	}),
+				this.log("SUCCESS", "GET_PUNISHMENTS_FOR_USER", `Got punishments for user ${userId} successful.`);
+				return cb({ status: "success", data: { punishments } });
+			});
+		}
+	),
 
 	/**
 	 * Returns a punishment by id
@@ -230,7 +236,7 @@ export default {
 	 * @param {string} punishmentId - the punishment id
 	 * @param {Function} cb - gets called with the result
 	 */
-	findOne: isAdminRequired(async function findOne(session, punishmentId, cb) {
+	findOne: useHasPermission("punishments.findOne", async function findOne(session, punishmentId, cb) {
 		const punishmentModel = await DBModule.runJob("GET_MODEL", { modelName: "punishment" }, this);
 
 		async.waterfall([next => punishmentModel.findOne({ _id: punishmentId }, next)], async (err, punishment) => {
@@ -257,7 +263,7 @@ export default {
 	 * @param {string} expiresAt - the time the ban expires
 	 * @param {Function} cb - gets called with the result
 	 */
-	banIP: isAdminRequired(function banIP(session, value, reason, expiresAt, cb) {
+	banIP: useHasPermission("punishments.banIP", function banIP(session, value, reason, expiresAt, cb) {
 		async.waterfall(
 			[
 				next => {

+ 153 - 137
backend/logic/actions/reports.js

@@ -1,6 +1,7 @@
 import async from "async";
 
-import { isAdminRequired, isLoginRequired } from "./hooks";
+import isLoginRequired from "./hooks/loginRequired";
+import { useHasPermission } from "./hooks/hasPermission";
 
 // eslint-disable-next-line
 import moduleManager from "../../index";
@@ -99,93 +100,98 @@ export default {
 	 * @param operator - the operator for queries
 	 * @param cb
 	 */
-	getData: isAdminRequired(async function getSet(session, page, pageSize, properties, sort, queries, operator, cb) {
-		async.waterfall(
-			[
-				next => {
-					DBModule.runJob(
-						"GET_DATA",
-						{
-							page,
-							pageSize,
-							properties,
-							sort,
-							queries,
-							operator,
-							modelName: "report",
-							blacklistedProperties: [],
-							specialProperties: {
-								createdBy: [
-									{
-										$addFields: {
-											createdByOID: {
-												$convert: {
-													input: "$createdBy",
-													to: "objectId",
-													onError: "unknown",
-													onNull: "unknown"
+	getData: useHasPermission(
+		"reports.getData",
+		async function getSet(session, page, pageSize, properties, sort, queries, operator, cb) {
+			async.waterfall(
+				[
+					next => {
+						DBModule.runJob(
+							"GET_DATA",
+							{
+								page,
+								pageSize,
+								properties,
+								sort,
+								queries,
+								operator,
+								modelName: "report",
+								blacklistedProperties: [],
+								specialProperties: {
+									createdBy: [
+										{
+											$addFields: {
+												createdByOID: {
+													$convert: {
+														input: "$createdBy",
+														to: "objectId",
+														onError: "unknown",
+														onNull: "unknown"
+													}
 												}
 											}
-										}
-									},
-									{
-										$lookup: {
-											from: "users",
-											localField: "createdByOID",
-											foreignField: "_id",
-											as: "createdByUser"
-										}
-									},
-									{
-										$unwind: {
-											path: "$createdByUser",
-											preserveNullAndEmptyArrays: true
-										}
-									},
-									{
-										$addFields: {
-											createdByUsername: {
-												$ifNull: ["$createdByUser.username", "unknown"]
+										},
+										{
+											$lookup: {
+												from: "users",
+												localField: "createdByOID",
+												foreignField: "_id",
+												as: "createdByUser"
+											}
+										},
+										{
+											$unwind: {
+												path: "$createdByUser",
+												preserveNullAndEmptyArrays: true
+											}
+										},
+										{
+											$addFields: {
+												createdByUsername: {
+													$ifNull: ["$createdByUser.username", "unknown"]
+												}
+											}
+										},
+										{
+											$project: {
+												createdByOID: 0,
+												createdByUser: 0
 											}
 										}
-									},
-									{
-										$project: {
-											createdByOID: 0,
-											createdByUser: 0
-										}
-									}
-								]
+									]
+								},
+								specialQueries: {
+									createdBy: newQuery => ({
+										$or: [newQuery, { createdByUsername: newQuery.createdBy }]
+									})
+								}
 							},
-							specialQueries: {
-								createdBy: newQuery => ({ $or: [newQuery, { createdByUsername: newQuery.createdBy }] })
-							}
-						},
-						this
-					)
-						.then(response => {
-							next(null, response);
-						})
-						.catch(err => {
-							next(err);
-						});
-				}
-			],
-			async (err, response) => {
-				if (err && err !== true) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log("ERROR", "REPORTS_GET_DATA", `Failed to get data from reports. "${err}"`);
-					return cb({ status: "error", message: err });
+							this
+						)
+							.then(response => {
+								next(null, response);
+							})
+							.catch(err => {
+								next(err);
+							});
+					}
+				],
+				async (err, response) => {
+					if (err && err !== true) {
+						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+						this.log("ERROR", "REPORTS_GET_DATA", `Failed to get data from reports. "${err}"`);
+						return cb({ status: "error", message: err });
+					}
+					this.log("SUCCESS", "REPORTS_GET_DATA", `Got data from reports successfully.`);
+					return cb({
+						status: "success",
+						message: "Successfully got data from reports.",
+						data: response
+					});
 				}
-				this.log("SUCCESS", "REPORTS_GET_DATA", `Got data from reports successfully.`);
-				return cb({
-					status: "success",
-					message: "Successfully got data from reports.",
-					data: response
-				});
-			}
-		);
-	}),
+			);
+		}
+	),
 
 	/**
 	 * Gets a specific report
@@ -194,7 +200,7 @@ export default {
 	 * @param {string} reportId - the id of the report to return
 	 * @param {Function} cb - gets called with the result
 	 */
-	findOne: isAdminRequired(async function findOne(session, reportId, cb) {
+	findOne: useHasPermission("reports.findOne", async function findOne(session, reportId, cb) {
 		const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
 		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
 
@@ -243,60 +249,70 @@ export default {
 	 * @param {string} songId - the id of the song to index reports for
 	 * @param {Function} cb - gets called with the result
 	 */
-	getReportsForSong: isAdminRequired(async function getReportsForSong(session, songId, cb) {
-		const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
-		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
-
-		async.waterfall(
-			[
-				next =>
-					reportModel.find({ "song._id": songId, resolved: false }).sort({ createdAt: "desc" }).exec(next),
-
-				(_reports, next) => {
-					const reports = [];
+	getReportsForSong: useHasPermission(
+		"reports.getReportsForSong",
+		async function getReportsForSong(session, songId, cb) {
+			const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
+			const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
+
+			async.waterfall(
+				[
+					next =>
+						reportModel
+							.find({ "song._id": songId, resolved: false })
+							.sort({ createdAt: "desc" })
+							.exec(next),
+
+					(_reports, next) => {
+						const reports = [];
+
+						async.each(
+							_reports,
+							(report, cb) => {
+								userModel
+									.findById(report.createdBy)
+									.select({ avatar: -1, name: -1, username: -1 })
+									.exec((err, user) => {
+										if (!user)
+											reports.push({
+												...report._doc,
+												createdBy: { _id: report.createdBy }
+											});
+										else
+											reports.push({
+												...report._doc,
+												createdBy: {
+													avatar: user.avatar,
+													name: user.name,
+													username: user.username,
+													_id: report.createdBy
+												}
+											});
 
-					async.each(
-						_reports,
-						(report, cb) => {
-							userModel
-								.findById(report.createdBy)
-								.select({ avatar: -1, name: -1, username: -1 })
-								.exec((err, user) => {
-									if (!user)
-										reports.push({
-											...report._doc,
-											createdBy: { _id: report.createdBy }
-										});
-									else
-										reports.push({
-											...report._doc,
-											createdBy: {
-												avatar: user.avatar,
-												name: user.name,
-												username: user.username,
-												_id: report.createdBy
-											}
-										});
+										return cb(err);
+									});
+							},
+							err => next(err, reports)
+						);
+					}
+				],
+				async (err, reports) => {
+					if (err) {
+						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+						this.log(
+							"ERROR",
+							"GET_REPORTS_FOR_SONG",
+							`Indexing reports for song "${songId}" failed. "${err}"`
+						);
+						return cb({ status: "error", message: err });
+					}
 
-									return cb(err);
-								});
-						},
-						err => next(err, reports)
-					);
-				}
-			],
-			async (err, reports) => {
-				if (err) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log("ERROR", "GET_REPORTS_FOR_SONG", `Indexing reports for song "${songId}" failed. "${err}"`);
-					return cb({ status: "error", message: err });
+					this.log("SUCCESS", "GET_REPORTS_FOR_SONG", `Indexing reports for song "${songId}" successful.`);
+					return cb({ status: "success", data: { reports } });
 				}
-
-				this.log("SUCCESS", "GET_REPORTS_FOR_SONG", `Indexing reports for song "${songId}" successful.`);
-				return cb({ status: "success", data: { reports } });
-			}
-		);
-	}),
+			);
+		}
+	),
 
 	/**
 	 * Gets all a users reports for a specific songId
@@ -380,7 +396,7 @@ export default {
 	 * @param {boolean} resolved - whether to set to resolved to true or false
 	 * @param {Function} cb - gets called with the result
 	 */
-	resolve: isAdminRequired(async function resolve(session, reportId, resolved, cb) {
+	resolve: useHasPermission("reports.resolve", async function resolve(session, reportId, resolved, cb) {
 		const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
 
 		async.waterfall(
@@ -445,7 +461,7 @@ export default {
 	 * @param {string} issueId - the id of the issue within the report
 	 * @param {Function} cb - gets called with the result
 	 */
-	toggleIssue: isAdminRequired(async function toggleIssue(session, reportId, issueId, cb) {
+	toggleIssue: useHasPermission("reports.toggleIssue", async function toggleIssue(session, reportId, issueId, cb) {
 		const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
 
 		async.waterfall(
@@ -594,7 +610,7 @@ export default {
 	 * @param {object} reportId - the id of the report item we want to remove
 	 * @param {Function} cb - gets called with the result
 	 */
-	remove: isAdminRequired(async function remove(session, reportId, cb) {
+	remove: useHasPermission("reports.remove", async function remove(session, reportId, cb) {
 		const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
 
 		async.waterfall(

+ 252 - 242
backend/logic/actions/songs.js

@@ -1,6 +1,7 @@
 import async from "async";
 
-import { isAdminRequired, isLoginRequired } from "./hooks";
+import isLoginRequired from "./hooks/loginRequired";
+import { useHasPermission } from "./hooks/hasPermission";
 
 // eslint-disable-next-line
 import moduleManager from "../../index";
@@ -47,7 +48,7 @@ export default {
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param cb
 	 */
-	length: isAdminRequired(async function length(session, cb) {
+	length: useHasPermission("songs.length", async function length(session, cb) {
 		const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
 		async.waterfall(
 			[
@@ -79,129 +80,132 @@ export default {
 	 * @param operator - the operator for queries
 	 * @param cb
 	 */
-	getData: isAdminRequired(async function getSet(session, page, pageSize, properties, sort, queries, operator, cb) {
-		async.waterfall(
-			[
-				next => {
-					DBModule.runJob(
-						"GET_DATA",
-						{
-							page,
-							pageSize,
-							properties,
-							sort,
-							queries,
-							operator,
-							modelName: "song",
-							blacklistedProperties: [],
-							specialProperties: {
-								requestedBy: [
-									{
-										$addFields: {
-											requestedByOID: {
-												$convert: {
-													input: "$requestedBy",
-													to: "objectId",
-													onError: "unknown",
-													onNull: "unknown"
+	getData: useHasPermission(
+		"songs.getData",
+		async function getSet(session, page, pageSize, properties, sort, queries, operator, cb) {
+			async.waterfall(
+				[
+					next => {
+						DBModule.runJob(
+							"GET_DATA",
+							{
+								page,
+								pageSize,
+								properties,
+								sort,
+								queries,
+								operator,
+								modelName: "song",
+								blacklistedProperties: [],
+								specialProperties: {
+									requestedBy: [
+										{
+											$addFields: {
+												requestedByOID: {
+													$convert: {
+														input: "$requestedBy",
+														to: "objectId",
+														onError: "unknown",
+														onNull: "unknown"
+													}
 												}
 											}
-										}
-									},
-									{
-										$lookup: {
-											from: "users",
-											localField: "requestedByOID",
-											foreignField: "_id",
-											as: "requestedByUser"
-										}
-									},
-									{
-										$addFields: {
-											requestedByUsername: {
-												$ifNull: ["$requestedByUser.username", "unknown"]
+										},
+										{
+											$lookup: {
+												from: "users",
+												localField: "requestedByOID",
+												foreignField: "_id",
+												as: "requestedByUser"
 											}
-										}
-									},
-									{
-										$project: {
-											requestedByOID: 0,
-											requestedByUser: 0
-										}
-									}
-								],
-								verifiedBy: [
-									{
-										$addFields: {
-											verifiedByOID: {
-												$convert: {
-													input: "$verifiedBy",
-													to: "objectId",
-													onError: "unknown",
-													onNull: "unknown"
+										},
+										{
+											$addFields: {
+												requestedByUsername: {
+													$ifNull: ["$requestedByUser.username", "unknown"]
 												}
 											}
-										}
-									},
-									{
-										$lookup: {
-											from: "users",
-											localField: "verifiedByOID",
-											foreignField: "_id",
-											as: "verifiedByUser"
-										}
-									},
-									{
-										$unwind: {
-											path: "$verifiedByUser",
-											preserveNullAndEmptyArrays: true
-										}
-									},
-									{
-										$addFields: {
-											verifiedByUsername: {
-												$ifNull: ["$verifiedByUser.username", "unknown"]
+										},
+										{
+											$project: {
+												requestedByOID: 0,
+												requestedByUser: 0
 											}
 										}
-									},
-									{
-										$project: {
-											verifiedByOID: 0,
-											verifiedByUser: 0
+									],
+									verifiedBy: [
+										{
+											$addFields: {
+												verifiedByOID: {
+													$convert: {
+														input: "$verifiedBy",
+														to: "objectId",
+														onError: "unknown",
+														onNull: "unknown"
+													}
+												}
+											}
+										},
+										{
+											$lookup: {
+												from: "users",
+												localField: "verifiedByOID",
+												foreignField: "_id",
+												as: "verifiedByUser"
+											}
+										},
+										{
+											$unwind: {
+												path: "$verifiedByUser",
+												preserveNullAndEmptyArrays: true
+											}
+										},
+										{
+											$addFields: {
+												verifiedByUsername: {
+													$ifNull: ["$verifiedByUser.username", "unknown"]
+												}
+											}
+										},
+										{
+											$project: {
+												verifiedByOID: 0,
+												verifiedByUser: 0
+											}
 										}
-									}
-								]
+									]
+								},
+								specialQueries: {
+									requestedBy: newQuery => ({
+										$or: [newQuery, { requestedByUsername: newQuery.requestedBy }]
+									}),
+									verifiedBy: newQuery => ({
+										$or: [newQuery, { verifiedByUsername: newQuery.verifiedBy }]
+									})
+								}
 							},
-							specialQueries: {
-								requestedBy: newQuery => ({
-									$or: [newQuery, { requestedByUsername: newQuery.requestedBy }]
-								}),
-								verifiedBy: newQuery => ({
-									$or: [newQuery, { verifiedByUsername: newQuery.verifiedBy }]
-								})
-							}
-						},
-						this
-					)
-						.then(response => {
-							next(null, response);
-						})
-						.catch(err => {
-							next(err);
-						});
-				}
-			],
-			async (err, response) => {
-				if (err) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log("ERROR", "SONGS_GET_DATA", `Failed to get data from songs. "${err}"`);
-					return cb({ status: "error", message: err });
+							this
+						)
+							.then(response => {
+								next(null, response);
+							})
+							.catch(err => {
+								next(err);
+							});
+					}
+				],
+				async (err, response) => {
+					if (err) {
+						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+						this.log("ERROR", "SONGS_GET_DATA", `Failed to get data from songs. "${err}"`);
+						return cb({ status: "error", message: err });
+					}
+					this.log("SUCCESS", "SONGS_GET_DATA", `Got data from songs successfully.`);
+					return cb({ status: "success", message: "Successfully got data from songs.", data: response });
 				}
-				this.log("SUCCESS", "SONGS_GET_DATA", `Got data from songs successfully.`);
-				return cb({ status: "success", message: "Successfully got data from songs.", data: response });
-			}
-		);
-	}),
+			);
+		}
+	),
 
 	/**
 	 * Updates all songs
@@ -209,7 +213,7 @@ export default {
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param cb
 	 */
-	updateAll: isAdminRequired(async function updateAll(session, cb) {
+	updateAll: useHasPermission("songs.updateAll", async function updateAll(session, cb) {
 		this.keepLongJob();
 		this.publishProgress({
 			status: "started",
@@ -266,7 +270,7 @@ export default {
 	 * @param {string} songId - the song id
 	 * @param {Function} cb
 	 */
-	getSongFromSongId: isAdminRequired(function getSongFromSongId(session, songId, cb) {
+	getSongFromSongId: useHasPermission("songs.getSongFromId", function getSongFromSongId(session, songId, cb) {
 		async.waterfall(
 			[
 				next => {
@@ -295,42 +299,45 @@ export default {
 	 * @param {Array} youtubeIds - the song ids
 	 * @param {Function} cb
 	 */
-	getSongsFromYoutubeIds: isAdminRequired(function getSongsFromYoutubeIds(session, youtubeIds, cb) {
-		async.waterfall(
-			[
-				next => {
-					SongsModule.runJob(
-						"GET_SONGS",
-						{
-							youtubeIds,
-							properties: [
-								"youtubeId",
-								"title",
-								"artists",
-								"thumbnail",
-								"duration",
-								"verified",
-								"_id",
-								"youtubeVideoId"
-							]
-						},
-						this
-					)
-						.then(response => next(null, response.songs))
-						.catch(err => next(err));
-				}
-			],
-			async (err, songs) => {
-				if (err) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log("ERROR", "SONGS_GET_SONGS_FROM_MUSARE_IDS", `Failed to get songs. "${err}"`);
-					return cb({ status: "error", message: err });
+	getSongsFromYoutubeIds: useHasPermission(
+		"songs.getSongsFromYoutubeIds",
+		function getSongsFromYoutubeIds(session, youtubeIds, cb) {
+			async.waterfall(
+				[
+					next => {
+						SongsModule.runJob(
+							"GET_SONGS",
+							{
+								youtubeIds,
+								properties: [
+									"youtubeId",
+									"title",
+									"artists",
+									"thumbnail",
+									"duration",
+									"verified",
+									"_id",
+									"youtubeVideoId"
+								]
+							},
+							this
+						)
+							.then(response => next(null, response.songs))
+							.catch(err => next(err));
+					}
+				],
+				async (err, songs) => {
+					if (err) {
+						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+						this.log("ERROR", "SONGS_GET_SONGS_FROM_MUSARE_IDS", `Failed to get songs. "${err}"`);
+						return cb({ status: "error", message: err });
+					}
+					this.log("SUCCESS", "SONGS_GET_SONGS_FROM_MUSARE_IDS", `Got songs successfully.`);
+					return cb({ status: "success", data: { songs } });
 				}
-				this.log("SUCCESS", "SONGS_GET_SONGS_FROM_MUSARE_IDS", `Got songs successfully.`);
-				return cb({ status: "success", data: { songs } });
-			}
-		);
-	}),
+			);
+		}
+	),
 
 	/**
 	 * Creates a song
@@ -339,7 +346,7 @@ export default {
 	 * @param {object} newSong - the song object
 	 * @param {Function} cb
 	 */
-	create: isAdminRequired(async function create(session, newSong, cb) {
+	create: useHasPermission("songs.create", async function create(session, newSong, cb) {
 		async.waterfall(
 			[
 				next => {
@@ -376,7 +383,7 @@ export default {
 	 * @param {object} song - the updated song object
 	 * @param {Function} cb
 	 */
-	update: isAdminRequired(async function update(session, songId, song, cb) {
+	update: useHasPermission("songs.update", async function update(session, songId, song, cb) {
 		const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
 		let existingSong = null;
 		async.waterfall(
@@ -453,7 +460,7 @@ export default {
 	 * @param songId - the song id
 	 * @param cb
 	 */
-	remove: isAdminRequired(async function remove(session, songId, cb) {
+	remove: useHasPermission("songs.remove", async function remove(session, songId, cb) {
 		const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
 		const stationModel = await DBModule.runJob("GET_MODEL", { modelName: "station" }, this);
 
@@ -717,7 +724,7 @@ export default {
 	 * @param songIds - array of song ids
 	 * @param cb
 	 */
-	removeMany: isAdminRequired(async function remove(session, songIds, cb) {
+	removeMany: useHasPermission("songs.removeMany", async function remove(session, songIds, cb) {
 		const successful = [];
 		const failed = [];
 
@@ -860,7 +867,7 @@ export default {
 	 * @param songId - the song id
 	 * @param cb
 	 */
-	verify: isAdminRequired(async function add(session, songId, cb) {
+	verify: useHasPermission("songs.verify", async function add(session, songId, cb) {
 		const SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
 		async.waterfall(
 			[
@@ -919,7 +926,7 @@ export default {
 	 * @param songIds - array of song ids
 	 * @param cb
 	 */
-	verifyMany: isAdminRequired(async function verifyMany(session, songIds, cb) {
+	verifyMany: useHasPermission("songs.verifyMany", async function verifyMany(session, songIds, cb) {
 		const successful = [];
 		const failed = [];
 
@@ -1015,7 +1022,7 @@ export default {
 	 * @param songId - the song id
 	 * @param cb
 	 */
-	unverify: isAdminRequired(async function add(session, songId, cb) {
+	unverify: useHasPermission("songs.unverify", async function add(session, songId, cb) {
 		const SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
 		async.waterfall(
 			[
@@ -1080,7 +1087,7 @@ export default {
 	 * @param songIds - array of song ids
 	 * @param cb
 	 */
-	unverifyMany: isAdminRequired(async function unverifyMany(session, songIds, cb) {
+	unverifyMany: useHasPermission("songs.unverifyMany", async function unverifyMany(session, songIds, cb) {
 		const successful = [];
 		const failed = [];
 
@@ -1183,7 +1190,7 @@ export default {
 	 * @param session
 	 * @param cb
 	 */
-	getGenres: isAdminRequired(function getGenres(session, cb) {
+	getGenres: useHasPermission("songs.getGenres", function getGenres(session, cb) {
 		async.waterfall(
 			[
 				next => {
@@ -1222,7 +1229,7 @@ export default {
 	 * @param songIds Array of songIds to apply genres to
 	 * @param cb
 	 */
-	editGenres: isAdminRequired(async function editGenres(session, method, genres, songIds, cb) {
+	editGenres: useHasPermission("songs.editGenres", async function editGenres(session, method, genres, songIds, cb) {
 		const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
 
 		this.keepLongJob();
@@ -1311,7 +1318,7 @@ export default {
 	 * @param session
 	 * @param cb
 	 */
-	getArtists: isAdminRequired(function getArtists(session, cb) {
+	getArtists: useHasPermission("songs.getArtists", function getArtists(session, cb) {
 		async.waterfall(
 			[
 				next => {
@@ -1350,88 +1357,91 @@ export default {
 	 * @param songIds Array of songIds to apply artists to
 	 * @param cb
 	 */
-	editArtists: isAdminRequired(async function editArtists(session, method, artists, songIds, cb) {
-		const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
-
-		this.keepLongJob();
-		this.publishProgress({
-			status: "started",
-			title: "Bulk editing artists",
-			message: "Updating artists.",
-			id: this.toString()
-		});
-		await CacheModule.runJob("RPUSH", { key: `longJobs.${session.userId}`, value: this.toString() }, this);
-		await CacheModule.runJob(
-			"PUB",
-			{
-				channel: "longJob.added",
-				value: { jobId: this.toString(), userId: session.userId }
-			},
-			this
-		);
-
-		async.waterfall(
-			[
-				next => {
-					songModel.find({ _id: { $in: songIds } }, next);
-				},
-
-				(songs, next) => {
-					const songsFound = songs.map(song => song._id);
-					if (songsFound.length > 0) next(null, songsFound);
-					else next("None of the specified songs were found.");
+	editArtists: useHasPermission(
+		"songs.editArtists",
+		async function editArtists(session, method, artists, songIds, cb) {
+			const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
+
+			this.keepLongJob();
+			this.publishProgress({
+				status: "started",
+				title: "Bulk editing artists",
+				message: "Updating artists.",
+				id: this.toString()
+			});
+			await CacheModule.runJob("RPUSH", { key: `longJobs.${session.userId}`, value: this.toString() }, this);
+			await CacheModule.runJob(
+				"PUB",
+				{
+					channel: "longJob.added",
+					value: { jobId: this.toString(), userId: session.userId }
 				},
+				this
+			);
+
+			async.waterfall(
+				[
+					next => {
+						songModel.find({ _id: { $in: songIds } }, next);
+					},
+
+					(songs, next) => {
+						const songsFound = songs.map(song => song._id);
+						if (songsFound.length > 0) next(null, songsFound);
+						else next("None of the specified songs were found.");
+					},
+
+					(songsFound, next) => {
+						const query = {};
+						if (method === "add") {
+							query.$addToSet = { artists: { $each: artists } };
+						} else if (method === "remove") {
+							query.$pullAll = { artists };
+						} else if (method === "replace") {
+							query.$set = { artists };
+						} else {
+							next("Invalid method.");
+							return;
+						}
 
-				(songsFound, next) => {
-					const query = {};
-					if (method === "add") {
-						query.$addToSet = { artists: { $each: artists } };
-					} else if (method === "remove") {
-						query.$pullAll = { artists };
-					} else if (method === "replace") {
-						query.$set = { artists };
+						this.publishProgress({
+							status: "update",
+							message: "Updating artists in MongoDB."
+						});
+						songModel.updateMany({ _id: { $in: songsFound } }, query, { runValidators: true }, err => {
+							if (err) {
+								next(err);
+								return;
+							}
+							SongsModule.runJob("UPDATE_SONGS", { songIds: songsFound });
+							next();
+						});
+					}
+				],
+				async err => {
+					if (err && err !== true) {
+						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+						this.log("ERROR", "EDIT_ARTISTS", `User ${session.userId} failed to edit artists. '${err}'`);
+						this.publishProgress({
+							status: "error",
+							message: err
+						});
+						cb({ status: "error", message: err });
 					} else {
-						next("Invalid method.");
-						return;
+						this.log("SUCCESS", "EDIT_ARTISTS", `User ${session.userId} has successfully edited artists.`);
+						this.publishProgress({
+							status: "success",
+							message: "Successfully edited artists."
+						});
+						cb({
+							status: "success",
+							message: "Successfully edited artists."
+						});
 					}
-
-					this.publishProgress({
-						status: "update",
-						message: "Updating artists in MongoDB."
-					});
-					songModel.updateMany({ _id: { $in: songsFound } }, query, { runValidators: true }, err => {
-						if (err) {
-							next(err);
-							return;
-						}
-						SongsModule.runJob("UPDATE_SONGS", { songIds: songsFound });
-						next();
-					});
-				}
-			],
-			async err => {
-				if (err && err !== true) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log("ERROR", "EDIT_ARTISTS", `User ${session.userId} failed to edit artists. '${err}'`);
-					this.publishProgress({
-						status: "error",
-						message: err
-					});
-					cb({ status: "error", message: err });
-				} else {
-					this.log("SUCCESS", "EDIT_ARTISTS", `User ${session.userId} has successfully edited artists.`);
-					this.publishProgress({
-						status: "success",
-						message: "Successfully edited artists."
-					});
-					cb({
-						status: "success",
-						message: "Successfully edited artists."
-					});
 				}
-			}
-		);
-	}),
+			);
+		}
+	),
 
 	/**
 	 * Gets a list of all tags
@@ -1439,7 +1449,7 @@ export default {
 	 * @param session
 	 * @param cb
 	 */
-	getTags: isAdminRequired(function getTags(session, cb) {
+	getTags: useHasPermission("songs.getTags", function getTags(session, cb) {
 		async.waterfall(
 			[
 				next => {
@@ -1478,7 +1488,7 @@ export default {
 	 * @param songIds Array of songIds to apply tags to
 	 * @param cb
 	 */
-	editTags: isAdminRequired(async function editTags(session, method, tags, songIds, cb) {
+	editTags: useHasPermission("songs.editTags", async function editTags(session, method, tags, songIds, cb) {
 		const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
 
 		this.keepLongJob();

+ 133 - 125
backend/logic/actions/stations.js

@@ -2,7 +2,9 @@ import async from "async";
 import mongoose from "mongoose";
 import config from "config";
 
-import { isLoginRequired, isOwnerRequired, isAdminRequired } from "./hooks";
+import { useHasPermission } from "./hooks/hasPermission";
+import isLoginRequired from "./hooks/loginRequired";
+import isOwnerRequired from "./hooks/ownerRequired";
 
 // eslint-disable-next-line
 import moduleManager from "../../index";
@@ -496,93 +498,96 @@ export default {
 	 * @param operator - the operator for queries
 	 * @param cb
 	 */
-	getData: isAdminRequired(async function getSet(session, page, pageSize, properties, sort, queries, operator, cb) {
-		async.waterfall(
-			[
-				next => {
-					DBModule.runJob(
-						"GET_DATA",
-						{
-							page,
-							pageSize,
-							properties,
-							sort,
-							queries,
-							operator,
-							modelName: "station",
-							blacklistedProperties: [],
-							specialProperties: {
-								owner: [
-									{
-										$addFields: {
-											ownerOID: {
-												$convert: {
-													input: "$owner",
-													to: "objectId",
-													onError: "unknown",
-													onNull: "unknown"
+	getData: useHasPermission(
+		"stations.getData",
+		async function getSet(session, page, pageSize, properties, sort, queries, operator, cb) {
+			async.waterfall(
+				[
+					next => {
+						DBModule.runJob(
+							"GET_DATA",
+							{
+								page,
+								pageSize,
+								properties,
+								sort,
+								queries,
+								operator,
+								modelName: "station",
+								blacklistedProperties: [],
+								specialProperties: {
+									owner: [
+										{
+											$addFields: {
+												ownerOID: {
+													$convert: {
+														input: "$owner",
+														to: "objectId",
+														onError: "unknown",
+														onNull: "unknown"
+													}
 												}
 											}
-										}
-									},
-									{
-										$lookup: {
-											from: "users",
-											localField: "ownerOID",
-											foreignField: "_id",
-											as: "ownerUser"
-										}
-									},
-									{
-										$unwind: {
-											path: "$ownerUser",
-											preserveNullAndEmptyArrays: true
-										}
-									},
-									{
-										$addFields: {
-											ownerUsername: {
-												$cond: [
-													{ $eq: [{ $type: "$owner" }, "string"] },
-													{ $ifNull: ["$ownerUser.username", "unknown"] },
-													"none"
-												]
+										},
+										{
+											$lookup: {
+												from: "users",
+												localField: "ownerOID",
+												foreignField: "_id",
+												as: "ownerUser"
+											}
+										},
+										{
+											$unwind: {
+												path: "$ownerUser",
+												preserveNullAndEmptyArrays: true
+											}
+										},
+										{
+											$addFields: {
+												ownerUsername: {
+													$cond: [
+														{ $eq: [{ $type: "$owner" }, "string"] },
+														{ $ifNull: ["$ownerUser.username", "unknown"] },
+														"none"
+													]
+												}
+											}
+										},
+										{
+											$project: {
+												ownerOID: 0,
+												ownerUser: 0
 											}
 										}
-									},
-									{
-										$project: {
-											ownerOID: 0,
-											ownerUser: 0
-										}
-									}
-								]
+									]
+								},
+								specialQueries: {
+									owner: newQuery => ({ $or: [newQuery, { ownerUsername: newQuery.owner }] })
+								}
 							},
-							specialQueries: {
-								owner: newQuery => ({ $or: [newQuery, { ownerUsername: newQuery.owner }] })
-							}
-						},
-						this
-					)
-						.then(response => {
-							next(null, response);
-						})
-						.catch(err => {
-							next(err);
-						});
-				}
-			],
-			async (err, response) => {
-				if (err) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log("ERROR", "STATIONS_GET_DATA", `Failed to get data from stations. "${err}"`);
-					return cb({ status: "error", message: err });
+							this
+						)
+							.then(response => {
+								next(null, response);
+							})
+							.catch(err => {
+								next(err);
+							});
+					}
+				],
+				async (err, response) => {
+					if (err) {
+						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+						this.log("ERROR", "STATIONS_GET_DATA", `Failed to get data from stations. "${err}"`);
+						return cb({ status: "error", message: err });
+					}
+					this.log("SUCCESS", "STATIONS_GET_DATA", `Got data from stations successfully.`);
+					return cb({ status: "success", message: "Successfully got data from stations.", data: response });
 				}
-				this.log("SUCCESS", "STATIONS_GET_DATA", `Got data from stations successfully.`);
-				return cb({ status: "success", message: "Successfully got data from stations.", data: response });
-			}
-		);
-	}),
+			);
+		}
+	),
 
 	/**
 	 * Obtains basic metadata of a station in order to format an activity
@@ -2468,51 +2473,54 @@ export default {
 	 * @param {object} session - the session object automatically added by socket.io
 	 * @param {Function} cb - gets called with the result
 	 */
-	clearEveryStationQueue: isAdminRequired(async function clearEveryStationQueue(session, cb) {
-		this.keepLongJob();
-		this.publishProgress({
-			status: "started",
-			title: "Clear every station queue",
-			message: "Clearing every station queue.",
-			id: this.toString()
-		});
-		await CacheModule.runJob("RPUSH", { key: `longJobs.${session.userId}`, value: this.toString() }, this);
-		await CacheModule.runJob(
-			"PUB",
-			{
-				channel: "longJob.added",
-				value: { jobId: this.toString(), userId: session.userId }
-			},
-			this
-		);
-
-		async.waterfall(
-			[
-				next => {
-					StationsModule.runJob("CLEAR_EVERY_STATION_QUEUE", {}, this)
-						.then(() => next())
-						.catch(next);
-				}
-			],
-			async err => {
-				if (err) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log("ERROR", "CLEAR_EVERY_STATION_QUEUE", `Clearing every station queue failed. "${err}"`);
+	clearEveryStationQueue: useHasPermission(
+		"stations.clearEveryStationQueue",
+		async function clearEveryStationQueue(session, cb) {
+			this.keepLongJob();
+			this.publishProgress({
+				status: "started",
+				title: "Clear every station queue",
+				message: "Clearing every station queue.",
+				id: this.toString()
+			});
+			await CacheModule.runJob("RPUSH", { key: `longJobs.${session.userId}`, value: this.toString() }, this);
+			await CacheModule.runJob(
+				"PUB",
+				{
+					channel: "longJob.added",
+					value: { jobId: this.toString(), userId: session.userId }
+				},
+				this
+			);
+
+			async.waterfall(
+				[
+					next => {
+						StationsModule.runJob("CLEAR_EVERY_STATION_QUEUE", {}, this)
+							.then(() => next())
+							.catch(next);
+					}
+				],
+				async err => {
+					if (err) {
+						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+						this.log("ERROR", "CLEAR_EVERY_STATION_QUEUE", `Clearing every station queue failed. "${err}"`);
+						this.publishProgress({
+							status: "error",
+							message: err
+						});
+						return cb({ status: "error", message: err });
+					}
+					this.log("SUCCESS", "CLEAR_EVERY_STATION_QUEUE", "Clearing every station queue was successful.");
 					this.publishProgress({
-						status: "error",
-						message: err
+						status: "success",
+						message: "Successfully cleared every station queue."
 					});
-					return cb({ status: "error", message: err });
+					return cb({ status: "success", message: "Successfully cleared every station queue." });
 				}
-				this.log("SUCCESS", "CLEAR_EVERY_STATION_QUEUE", "Clearing every station queue was successful.");
-				this.publishProgress({
-					status: "success",
-					message: "Successfully cleared every station queue."
-				});
-				return cb({ status: "success", message: "Successfully cleared every station queue." });
-			}
-		);
-	}),
+			);
+		}
+	),
 
 	/**
 	 * Reset a station queue
@@ -2521,7 +2529,7 @@ export default {
 	 * @param {string} stationId - the station id
 	 * @param {Function} cb - gets called with the result
 	 */
-	resetQueue: isAdminRequired(async function resetQueue(session, stationId, cb) {
+	resetQueue: useHasPermission("stations.resetQueue", async function resetQueue(session, stationId, cb) {
 		async.waterfall(
 			[
 				next => {

+ 174 - 164
backend/logic/actions/users.js

@@ -6,7 +6,8 @@ import mongoose from "mongoose";
 import axios from "axios";
 import bcrypt from "bcrypt";
 import sha256 from "sha256";
-import { isAdminRequired, isLoginRequired } from "./hooks";
+import isLoginRequired from "./hooks/loginRequired";
+import { useHasPermission } from "./hooks/hasPermission";
 
 // eslint-disable-next-line
 import moduleManager from "../../index";
@@ -245,71 +246,74 @@ export default {
 	 * @param operator - the operator for queries
 	 * @param cb
 	 */
-	getData: isAdminRequired(async function getSet(session, page, pageSize, properties, sort, queries, operator, cb) {
-		async.waterfall(
-			[
-				next => {
-					DBModule.runJob(
-						"GET_DATA",
-						{
-							page,
-							pageSize,
-							properties,
-							sort,
-							queries,
-							operator,
-							modelName: "user",
-							blacklistedProperties: [
-								"services.password.password",
-								"services.password.reset.code",
-								"services.password.reset.expires",
-								"services.password.set.code",
-								"services.password.set.expires",
-								"services.github.access_token",
-								"email.verificationToken"
-							],
-							specialProperties: {
-								hasPassword: [
-									{
-										$addFields: {
-											hasPassword: {
-												$cond: [
-													{ $eq: [{ $type: "$services.password.password" }, "string"] },
-													true,
-													false
-												]
+	getData: useHasPermission(
+		"users.getData",
+		async function getSet(session, page, pageSize, properties, sort, queries, operator, cb) {
+			async.waterfall(
+				[
+					next => {
+						DBModule.runJob(
+							"GET_DATA",
+							{
+								page,
+								pageSize,
+								properties,
+								sort,
+								queries,
+								operator,
+								modelName: "user",
+								blacklistedProperties: [
+									"services.password.password",
+									"services.password.reset.code",
+									"services.password.reset.expires",
+									"services.password.set.code",
+									"services.password.set.expires",
+									"services.github.access_token",
+									"email.verificationToken"
+								],
+								specialProperties: {
+									hasPassword: [
+										{
+											$addFields: {
+												hasPassword: {
+													$cond: [
+														{ $eq: [{ $type: "$services.password.password" }, "string"] },
+														true,
+														false
+													]
+												}
 											}
 										}
-									}
-								]
+									]
+								},
+								specialQueries: {}
 							},
-							specialQueries: {}
-						},
-						this
-					)
-						.then(response => {
-							next(null, response);
-						})
-						.catch(err => {
-							next(err);
-						});
-				}
-			],
-			async (err, response) => {
-				if (err && err !== true) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log("ERROR", "USERS_GET_DATA", `Failed to get data from users. "${err}"`);
-					return cb({ status: "error", message: err });
+							this
+						)
+							.then(response => {
+								next(null, response);
+							})
+							.catch(err => {
+								next(err);
+							});
+					}
+				],
+				async (err, response) => {
+					if (err && err !== true) {
+						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+						this.log("ERROR", "USERS_GET_DATA", `Failed to get data from users. "${err}"`);
+						return cb({ status: "error", message: err });
+					}
+					this.log("SUCCESS", "USERS_GET_DATA", `Got data from users successfully.`);
+					return cb({
+						status: "success",
+						message: "Successfully got data from users.",
+						data: response
+					});
 				}
-				this.log("SUCCESS", "USERS_GET_DATA", `Got data from users successfully.`);
-				return cb({
-					status: "success",
-					message: "Successfully got data from users.",
-					data: response
-				});
-			}
-		);
-	}),
+			);
+		}
+	),
 
 	/**
 	 * Removes all data held on a user, including their ability to login
@@ -541,7 +545,7 @@ export default {
 	 * @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) {
+	adminRemove: useHasPermission("users.adminRemove", 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);
@@ -1855,7 +1859,7 @@ export default {
 	 * @param {string} userId - the userId of the person we are trying to get the username from
 	 * @param {Function} cb - gets called with the result
 	 */
-	getUserFromId: isAdminRequired(async function getUserFromId(session, userId, cb) {
+	getUserFromId: useHasPermission("users.getUserFromId", async function getUserFromId(session, userId, cb) {
 		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
 		userModel
 			.findById(userId)
@@ -2458,7 +2462,7 @@ export default {
 	 * @param {string} newRole - the new role
 	 * @param {Function} cb - gets called with the result
 	 */
-	updateRole: isAdminRequired(async function updateRole(session, updatingUserId, newRole, cb) {
+	updateRole: useHasPermission("users.updateRole", async function updateRole(session, updatingUserId, newRole, cb) {
 		newRole = newRole.toLowerCase();
 		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
 
@@ -2989,73 +2993,76 @@ export default {
 	 * @param {string} email - the email of the user for which the password reset is intended
 	 * @param {Function} cb - gets called with the result
 	 */
-	adminRequestPasswordReset: isAdminRequired(async function adminRequestPasswordReset(session, userId, cb) {
-		const code = await UtilsModule.runJob("GENERATE_RANDOM_STRING", { length: 8 }, this);
-		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
-
-		const resetPasswordRequestSchema = await MailModule.runJob(
-			"GET_SCHEMA",
-			{ schemaName: "resetPasswordRequest" },
-			this
-		);
-
-		async.waterfall(
-			[
-				next => userModel.findOne({ _id: userId }, next),
-
-				(user, next) => {
-					if (!user) return next("User not found.");
-					if (!user.services.password || !user.services.password.password)
-						return next("User does not have a password set, and probably uses GitHub to log in.");
-					return next();
-				},
+	adminRequestPasswordReset: useHasPermission(
+		"users.adminRequestPasswordReset",
+		async function adminRequestPasswordReset(session, userId, cb) {
+			const code = await UtilsModule.runJob("GENERATE_RANDOM_STRING", { length: 8 }, this);
+			const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
+
+			const resetPasswordRequestSchema = await MailModule.runJob(
+				"GET_SCHEMA",
+				{ schemaName: "resetPasswordRequest" },
+				this
+			);
+
+			async.waterfall(
+				[
+					next => userModel.findOne({ _id: userId }, next),
+
+					(user, next) => {
+						if (!user) return next("User not found.");
+						if (!user.services.password || !user.services.password.password)
+							return next("User does not have a password set, and probably uses GitHub to log in.");
+						return next();
+					},
 
-				next => {
-					const expires = new Date();
-					expires.setDate(expires.getDate() + 1);
-					userModel.findOneAndUpdate(
-						{ _id: userId },
-						{
-							$set: {
-								"services.password.reset": {
-									code,
-									expires
+					next => {
+						const expires = new Date();
+						expires.setDate(expires.getDate() + 1);
+						userModel.findOneAndUpdate(
+							{ _id: userId },
+							{
+								$set: {
+									"services.password.reset": {
+										code,
+										expires
+									}
 								}
-							}
-						},
-						{ runValidators: true },
-						next
-					);
-				},
+							},
+							{ runValidators: true },
+							next
+						);
+					},
+
+					(user, next) => {
+						resetPasswordRequestSchema(user.email.address, user.username, code, next);
+					}
+				],
+				async err => {
+					if (err && err !== true) {
+						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+						this.log(
+							"ERROR",
+							"ADMINREQUEST_PASSWORD_RESET",
+							`User '${userId}' failed to get a password reset. '${err}'`
+						);
+						return cb({ status: "error", message: err });
+					}
 
-				(user, next) => {
-					resetPasswordRequestSchema(user.email.address, user.username, code, next);
-				}
-			],
-			async err => {
-				if (err && err !== true) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
 					this.log(
-						"ERROR",
-						"ADMINREQUEST_PASSWORD_RESET",
-						`User '${userId}' failed to get a password reset. '${err}'`
+						"SUCCESS",
+						"ADMIN_REQUEST_PASSWORD_RESET",
+						`User '${userId}' successfully got sent a password reset.`
 					);
-					return cb({ status: "error", message: err });
-				}
 
-				this.log(
-					"SUCCESS",
-					"ADMIN_REQUEST_PASSWORD_RESET",
-					`User '${userId}' successfully got sent a password reset.`
-				);
-
-				return cb({
-					status: "success",
-					message: "Successfully requested password reset for user."
-				});
-			}
-		);
-	}),
+					return cb({
+						status: "success",
+						message: "Successfully requested password reset for user."
+					});
+				}
+			);
+		}
+	),
 
 	/**
 	 * Verifies a reset code
@@ -3176,48 +3183,51 @@ export default {
 	 * @param {string} userId - the user id of the person to resend the email to
 	 * @param {Function} cb - gets called with the result
 	 */
-	resendVerifyEmail: isAdminRequired(async function resendVerifyEmail(session, userId, cb) {
-		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
-		const verifyEmailSchema = await MailModule.runJob("GET_SCHEMA", { schemaName: "verifyEmail" }, this);
-
-		async.waterfall(
-			[
-				next => userModel.findOne({ _id: userId }, next),
+	resendVerifyEmail: useHasPermission(
+		"users.resendVerifyEmail",
+		async function resendVerifyEmail(session, userId, cb) {
+			const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
+			const verifyEmailSchema = await MailModule.runJob("GET_SCHEMA", { schemaName: "verifyEmail" }, this);
+
+			async.waterfall(
+				[
+					next => userModel.findOne({ _id: userId }, next),
+
+					(user, next) => {
+						if (!user) return next("User not found.");
+						if (user.email.verified) return next("The user's email is already verified.");
+						return next(null, user);
+					},
 
-				(user, next) => {
-					if (!user) return next("User not found.");
-					if (user.email.verified) return next("The user's email is already verified.");
-					return next(null, user);
-				},
+					(user, next) => {
+						verifyEmailSchema(user.email.address, user.username, user.email.verificationToken, err => {
+							next(err);
+						});
+					}
+				],
+				async err => {
+					if (err && err !== true) {
+						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+
+						this.log(
+							"ERROR",
+							"RESEND_VERIFY_EMAIL",
+							`Couldn't resend verify email for user "${userId}". '${err}'`
+						);
 
-				(user, next) => {
-					verifyEmailSchema(user.email.address, user.username, user.email.verificationToken, err => {
-						next(err);
-					});
-				}
-			],
-			async err => {
-				if (err && err !== true) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+						return cb({ status: "error", message: err });
+					}
 
-					this.log(
-						"ERROR",
-						"RESEND_VERIFY_EMAIL",
-						`Couldn't resend verify email for user "${userId}". '${err}'`
-					);
+					this.log("SUCCESS", "RESEND_VERIFY_EMAIL", `Resent verify email for user "${userId}".`);
 
-					return cb({ status: "error", message: err });
+					return cb({
+						status: "success",
+						message: "Email resent successfully."
+					});
 				}
-
-				this.log("SUCCESS", "RESEND_VERIFY_EMAIL", `Resent verify email for user "${userId}".`);
-
-				return cb({
-					status: "success",
-					message: "Email resent successfully."
-				});
-			}
-		);
-	}),
+			);
+		}
+	),
 
 	/**
 	 * Bans a user by userId
@@ -3228,7 +3238,7 @@ export default {
 	 * @param {string} expiresAt - the time the ban expires
 	 * @param {Function} cb - gets called with the result
 	 */
-	banUserById: isAdminRequired(function banUserById(session, userId, reason, expiresAt, cb) {
+	banUserById: useHasPermission("users.banUserById", function banUserById(session, userId, reason, expiresAt, cb) {
 		async.waterfall(
 			[
 				next => {

+ 3 - 3
backend/logic/actions/utils.js

@@ -1,6 +1,6 @@
 import async from "async";
 
-import { isAdminRequired } from "./hooks";
+import { useHasPermission } from "./hooks/hasPermission";
 
 // eslint-disable-next-line
 import moduleManager from "../../index";
@@ -9,7 +9,7 @@ const UtilsModule = moduleManager.modules.utils;
 const WSModule = moduleManager.modules.ws;
 
 export default {
-	getModules: isAdminRequired(function getModules(session, cb) {
+	getModules: useHasPermission("utils.getModules", function getModules(session, cb) {
 		async.waterfall(
 			[
 				next => {
@@ -51,7 +51,7 @@ export default {
 		);
 	}),
 
-	getModule: isAdminRequired(function getModule(session, moduleName, cb) {
+	getModule: useHasPermission("utils.getModule", function getModule(session, moduleName, cb) {
 		async.waterfall(
 			[
 				next => {

+ 316 - 322
backend/logic/actions/youtube.js

@@ -1,7 +1,8 @@
 import mongoose from "mongoose";
 import async from "async";
 
-import { isAdminRequired, isLoginRequired } from "./hooks";
+import isLoginRequired from "./hooks/loginRequired";
+import { useHasPermission } from "./hooks/hasPermission";
 
 // eslint-disable-next-line
 import moduleManager from "../../index";
@@ -18,7 +19,7 @@ export default {
 	 *
 	 * @returns {{status: string, data: object}}
 	 */
-	getQuotaStatus: isAdminRequired(function getQuotaStatus(session, fromDate, cb) {
+	getQuotaStatus: useHasPermission("youtube.getQuotaStatus", function getQuotaStatus(session, fromDate, cb) {
 		YouTubeModule.runJob("GET_QUOTA_STATUS", { fromDate }, this)
 			.then(response => {
 				this.log("SUCCESS", "YOUTUBE_GET_QUOTA_STATUS", `Getting quota status was successful.`);
@@ -41,29 +42,25 @@ export default {
 	 * @param dataType - either usage or count
 	 * @returns {{status: string, data: object}}
 	 */
-	getQuotaChartData: isAdminRequired(function getQuotaChartData(
-		session,
-		timePeriod,
-		startDate,
-		endDate,
-		dataType,
-		cb
-	) {
-		YouTubeModule.runJob(
-			"GET_QUOTA_CHART_DATA",
-			{ timePeriod, startDate: new Date(startDate), endDate: new Date(endDate), dataType },
-			this
-		)
-			.then(data => {
-				this.log("SUCCESS", "YOUTUBE_GET_QUOTA_CHART_DATA", `Getting quota chart data was successful.`);
-				return cb({ status: "success", data });
-			})
-			.catch(async err => {
-				err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-				this.log("ERROR", "YOUTUBE_GET_QUOTA_CHART_DATA", `Getting quota chart data failed. "${err}"`);
-				return cb({ status: "error", message: err });
-			});
-	}),
+	getQuotaChartData: useHasPermission(
+		"youtube.getQuotaChartData",
+		function getQuotaChartData(session, timePeriod, startDate, endDate, dataType, cb) {
+			YouTubeModule.runJob(
+				"GET_QUOTA_CHART_DATA",
+				{ timePeriod, startDate: new Date(startDate), endDate: new Date(endDate), dataType },
+				this
+			)
+				.then(data => {
+					this.log("SUCCESS", "YOUTUBE_GET_QUOTA_CHART_DATA", `Getting quota chart data was successful.`);
+					return cb({ status: "success", data });
+				})
+				.catch(async err => {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+					this.log("ERROR", "YOUTUBE_GET_QUOTA_CHART_DATA", `Getting quota chart data failed. "${err}"`);
+					return cb({ status: "error", message: err });
+				});
+		}
+	),
 
 	/**
 	 * Gets api requests, used in the admin youtube page by the AdvancedTable component
@@ -77,65 +74,59 @@ export default {
 	 * @param operator - the operator for queries
 	 * @param cb
 	 */
-	getApiRequests: isAdminRequired(async function getApiRequests(
-		session,
-		page,
-		pageSize,
-		properties,
-		sort,
-		queries,
-		operator,
-		cb
-	) {
-		async.waterfall(
-			[
-				next => {
-					DBModule.runJob(
-						"GET_DATA",
-						{
-							page,
-							pageSize,
-							properties,
-							sort,
-							queries,
-							operator,
-							modelName: "youtubeApiRequest",
-							blacklistedProperties: [],
-							specialProperties: {},
-							specialQueries: {}
-						},
-						this
-					)
-						.then(response => {
-							next(null, response);
-						})
-						.catch(err => {
-							next(err);
-						});
-				}
-			],
-			async (err, response) => {
-				if (err && err !== true) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log("ERROR", "YOUTUBE_GET_API_REQUESTS", `Failed to get YouTube api requests. "${err}"`);
-					return cb({ status: "error", message: err });
+	getApiRequests: useHasPermission(
+		"youtube.getApiRequests",
+		async function getApiRequests(session, page, pageSize, properties, sort, queries, operator, cb) {
+			async.waterfall(
+				[
+					next => {
+						DBModule.runJob(
+							"GET_DATA",
+							{
+								page,
+								pageSize,
+								properties,
+								sort,
+								queries,
+								operator,
+								modelName: "youtubeApiRequest",
+								blacklistedProperties: [],
+								specialProperties: {},
+								specialQueries: {}
+							},
+							this
+						)
+							.then(response => {
+								next(null, response);
+							})
+							.catch(err => {
+								next(err);
+							});
+					}
+				],
+				async (err, response) => {
+					if (err && err !== true) {
+						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+						this.log("ERROR", "YOUTUBE_GET_API_REQUESTS", `Failed to get YouTube api requests. "${err}"`);
+						return cb({ status: "error", message: err });
+					}
+					this.log("SUCCESS", "YOUTUBE_GET_API_REQUESTS", `Fetched YouTube api requests successfully.`);
+					return cb({
+						status: "success",
+						message: "Successfully fetched YouTube api requests.",
+						data: response
+					});
 				}
-				this.log("SUCCESS", "YOUTUBE_GET_API_REQUESTS", `Fetched YouTube api requests successfully.`);
-				return cb({
-					status: "success",
-					message: "Successfully fetched YouTube api requests.",
-					data: response
-				});
-			}
-		);
-	}),
+			);
+		}
+	),
 
 	/**
 	 * Returns a specific api request
 	 *
 	 * @returns {{status: string, data: object}}
 	 */
-	getApiRequest: isAdminRequired(function getApiRequest(session, apiRequestId, cb) {
+	getApiRequest: useHasPermission("youtube.getApiRequest", function getApiRequest(session, apiRequestId, cb) {
 		if (!mongoose.Types.ObjectId.isValid(apiRequestId))
 			return cb({ status: "error", message: "Api request id is not a valid ObjectId." });
 
@@ -164,78 +155,84 @@ export default {
 	 *
 	 * @returns {{status: string, data: object}}
 	 */
-	resetStoredApiRequests: isAdminRequired(async function resetStoredApiRequests(session, cb) {
-		this.keepLongJob();
-		this.publishProgress({
-			status: "started",
-			title: "Reset stored API requests",
-			message: "Resetting stored API requests.",
-			id: this.toString()
-		});
-		await CacheModule.runJob("RPUSH", { key: `longJobs.${session.userId}`, value: this.toString() }, this);
-		await CacheModule.runJob(
-			"PUB",
-			{
-				channel: "longJob.added",
-				value: { jobId: this.toString(), userId: session.userId }
-			},
-			this
-		);
+	resetStoredApiRequests: useHasPermission(
+		"youtube.resetStoredApiRequests",
+		async function resetStoredApiRequests(session, cb) {
+			this.keepLongJob();
+			this.publishProgress({
+				status: "started",
+				title: "Reset stored API requests",
+				message: "Resetting stored API requests.",
+				id: this.toString()
+			});
+			await CacheModule.runJob("RPUSH", { key: `longJobs.${session.userId}`, value: this.toString() }, this);
+			await CacheModule.runJob(
+				"PUB",
+				{
+					channel: "longJob.added",
+					value: { jobId: this.toString(), userId: session.userId }
+				},
+				this
+			);
 
-		YouTubeModule.runJob("RESET_STORED_API_REQUESTS", {}, this)
-			.then(() => {
-				this.log(
-					"SUCCESS",
-					"YOUTUBE_RESET_STORED_API_REQUESTS",
-					`Resetting stored API requests was successful.`
-				);
-				this.publishProgress({
-					status: "success",
-					message: "Successfully reset stored YouTube API requests."
-				});
-				return cb({ status: "success", message: "Successfully reset stored YouTube API requests" });
-			})
-			.catch(async err => {
-				err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-				this.log(
-					"ERROR",
-					"YOUTUBE_RESET_STORED_API_REQUESTS",
-					`Resetting stored API requests failed. "${err}"`
-				);
-				this.publishProgress({
-					status: "error",
-					message: err
+			YouTubeModule.runJob("RESET_STORED_API_REQUESTS", {}, this)
+				.then(() => {
+					this.log(
+						"SUCCESS",
+						"YOUTUBE_RESET_STORED_API_REQUESTS",
+						`Resetting stored API requests was successful.`
+					);
+					this.publishProgress({
+						status: "success",
+						message: "Successfully reset stored YouTube API requests."
+					});
+					return cb({ status: "success", message: "Successfully reset stored YouTube API requests" });
+				})
+				.catch(async err => {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+					this.log(
+						"ERROR",
+						"YOUTUBE_RESET_STORED_API_REQUESTS",
+						`Resetting stored API requests failed. "${err}"`
+					);
+					this.publishProgress({
+						status: "error",
+						message: err
+					});
+					return cb({ status: "error", message: err });
 				});
-				return cb({ status: "error", message: err });
-			});
-	}),
+		}
+	),
 
 	/**
 	 * Remove stored API requests
 	 *
 	 * @returns {{status: string, data: object}}
 	 */
-	removeStoredApiRequest: isAdminRequired(function removeStoredApiRequest(session, requestId, cb) {
-		YouTubeModule.runJob("REMOVE_STORED_API_REQUEST", { requestId }, this)
-			.then(() => {
-				this.log(
-					"SUCCESS",
-					"YOUTUBE_REMOVE_STORED_API_REQUEST",
-					`Removing stored API request "${requestId}" was successful.`
-				);
+	removeStoredApiRequest: useHasPermission(
+		"youtube.removeStoredApiRequest",
+		function removeStoredApiRequest(session, requestId, cb) {
+			YouTubeModule.runJob("REMOVE_STORED_API_REQUEST", { requestId }, this)
+				.then(() => {
+					this.log(
+						"SUCCESS",
+						"YOUTUBE_REMOVE_STORED_API_REQUEST",
+						`Removing stored API request "${requestId}" was successful.`
+					);
 
-				return cb({ status: "success", message: "Successfully removed stored YouTube API request" });
-			})
-			.catch(async err => {
-				err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-				this.log(
-					"ERROR",
-					"YOUTUBE_REMOVE_STORED_API_REQUEST",
-					`Removing stored API request "${requestId}" failed. "${err}"`
-				);
-				return cb({ status: "error", message: err });
-			});
-	}),
+					return cb({ status: "success", message: "Successfully removed stored YouTube API request" });
+				})
+				.catch(async err => {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+					this.log(
+						"ERROR",
+						"YOUTUBE_REMOVE_STORED_API_REQUEST",
+						`Removing stored API request "${requestId}" failed. "${err}"`
+					);
+					return cb({ status: "error", message: err });
+				});
+		}
+	),
 
 	/**
 	 * Gets videos, used in the admin youtube page by the AdvancedTable component
@@ -249,97 +246,91 @@ export default {
 	 * @param operator - the operator for queries
 	 * @param cb
 	 */
-	getVideos: isAdminRequired(async function getVideos(
-		session,
-		page,
-		pageSize,
-		properties,
-		sort,
-		queries,
-		operator,
-		cb
-	) {
-		async.waterfall(
-			[
-				next => {
-					DBModule.runJob(
-						"GET_DATA",
-						{
-							page,
-							pageSize,
-							properties,
-							sort,
-							queries,
-							operator,
-							modelName: "youtubeVideo",
-							blacklistedProperties: [],
-							specialProperties: {},
-							specialQueries: {},
-							specialFilters: {
-								importJob: importJobId => [
-									{
-										$lookup: {
-											from: "importjobs",
-											let: { youtubeId: "$youtubeId" },
-											pipeline: [
-												{
-													$match: {
-														_id: mongoose.Types.ObjectId(importJobId)
-													}
-												},
-												{
-													$addFields: {
-														importJob: {
-															$in: ["$$youtubeId", "$response.successfulVideoIds"]
+	getVideos: useHasPermission(
+		"youtube.getVideos",
+		async function getVideos(session, page, pageSize, properties, sort, queries, operator, cb) {
+			async.waterfall(
+				[
+					next => {
+						DBModule.runJob(
+							"GET_DATA",
+							{
+								page,
+								pageSize,
+								properties,
+								sort,
+								queries,
+								operator,
+								modelName: "youtubeVideo",
+								blacklistedProperties: [],
+								specialProperties: {},
+								specialQueries: {},
+								specialFilters: {
+									importJob: importJobId => [
+										{
+											$lookup: {
+												from: "importjobs",
+												let: { youtubeId: "$youtubeId" },
+												pipeline: [
+													{
+														$match: {
+															_id: mongoose.Types.ObjectId(importJobId)
+														}
+													},
+													{
+														$addFields: {
+															importJob: {
+																$in: ["$$youtubeId", "$response.successfulVideoIds"]
+															}
+														}
+													},
+													{
+														$project: {
+															importJob: 1,
+															_id: 0
 														}
 													}
-												},
-												{
-													$project: {
-														importJob: 1,
-														_id: 0
-													}
-												}
-											],
-											as: "importJob"
-										}
-									},
-									{
-										$unwind: "$importJob"
-									},
-									{
-										$set: {
-											importJob: "$importJob.importJob"
+												],
+												as: "importJob"
+											}
+										},
+										{
+											$unwind: "$importJob"
+										},
+										{
+											$set: {
+												importJob: "$importJob.importJob"
+											}
 										}
-									}
-								]
-							}
-						},
-						this
-					)
-						.then(response => {
-							next(null, response);
-						})
-						.catch(err => {
-							next(err);
-						});
-				}
-			],
-			async (err, response) => {
-				if (err && err !== true) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log("ERROR", "YOUTUBE_GET_VIDEOS", `Failed to get YouTube videos. "${err}"`);
-					return cb({ status: "error", message: err });
+									]
+								}
+							},
+							this
+						)
+							.then(response => {
+								next(null, response);
+							})
+							.catch(err => {
+								next(err);
+							});
+					}
+				],
+				async (err, response) => {
+					if (err && err !== true) {
+						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+						this.log("ERROR", "YOUTUBE_GET_VIDEOS", `Failed to get YouTube videos. "${err}"`);
+						return cb({ status: "error", message: err });
+					}
+					this.log("SUCCESS", "YOUTUBE_GET_VIDEOS", `Fetched YouTube videos successfully.`);
+					return cb({
+						status: "success",
+						message: "Successfully fetched YouTube videos.",
+						data: response
+					});
 				}
-				this.log("SUCCESS", "YOUTUBE_GET_VIDEOS", `Fetched YouTube videos successfully.`);
-				return cb({
-					status: "success",
-					message: "Successfully fetched YouTube videos.",
-					data: response
-				});
-			}
-		);
-	}),
+			);
+		}
+	),
 
 	/**
 	 * Get a YouTube video
@@ -365,7 +356,7 @@ export default {
 	 *
 	 * @returns {{status: string, data: object}}
 	 */
-	removeVideos: isAdminRequired(async function removeVideos(session, videoIds, cb) {
+	removeVideos: useHasPermission("youtube.removeVideos", async function removeVideos(session, videoIds, cb) {
 		this.keepLongJob();
 		this.publishProgress({
 			status: "started",
@@ -446,110 +437,113 @@ export default {
 	 * @param {boolean} musicOnly - whether to return videos
 	 * @param {Function} cb - gets called with the result
 	 */
-	requestSetAdmin: isAdminRequired(async function requestSetAdmin(session, url, musicOnly, returnVideos, cb) {
-		const importJobModel = await DBModule.runJob("GET_MODEL", { modelName: "importJob" }, this);
+	requestSetAdmin: useHasPermission(
+		"youtube.requestSetAdmin",
+		async function requestSetAdmin(session, url, musicOnly, returnVideos, cb) {
+			const importJobModel = await DBModule.runJob("GET_MODEL", { modelName: "importJob" }, this);
 
-		this.keepLongJob();
-		this.publishProgress({
-			status: "started",
-			title: "Import playlist",
-			message: "Importing playlist.",
-			id: this.toString()
-		});
-		await CacheModule.runJob("RPUSH", { key: `longJobs.${session.userId}`, value: this.toString() }, this);
-		await CacheModule.runJob(
-			"PUB",
-			{
-				channel: "longJob.added",
-				value: { jobId: this.toString(), userId: session.userId }
-			},
-			this
-		);
+			this.keepLongJob();
+			this.publishProgress({
+				status: "started",
+				title: "Import playlist",
+				message: "Importing playlist.",
+				id: this.toString()
+			});
+			await CacheModule.runJob("RPUSH", { key: `longJobs.${session.userId}`, value: this.toString() }, this);
+			await CacheModule.runJob(
+				"PUB",
+				{
+					channel: "longJob.added",
+					value: { jobId: this.toString(), userId: session.userId }
+				},
+				this
+			);
 
-		async.waterfall(
-			[
-				next => {
-					importJobModel.create(
-						{
-							type: "youtube",
-							query: {
-								url,
-								musicOnly
+			async.waterfall(
+				[
+					next => {
+						importJobModel.create(
+							{
+								type: "youtube",
+								query: {
+									url,
+									musicOnly
+								},
+								status: "in-progress",
+								response: {},
+								requestedBy: session.userId,
+								requestedAt: Date.now()
 							},
-							status: "in-progress",
-							response: {},
-							requestedBy: session.userId,
-							requestedAt: Date.now()
-						},
-						next
-					);
-				},
+							next
+						);
+					},
 
-				(importJob, next) => {
-					YouTubeModule.runJob("REQUEST_SET", { url, musicOnly, returnVideos }, this)
-						.then(response => {
-							next(null, importJob, response);
-						})
-						.catch(err => {
-							next(err, importJob);
-						});
-				},
+					(importJob, next) => {
+						YouTubeModule.runJob("REQUEST_SET", { url, musicOnly, returnVideos }, this)
+							.then(response => {
+								next(null, importJob, response);
+							})
+							.catch(err => {
+								next(err, importJob);
+							});
+					},
 
-				(importJob, response, next) => {
-					importJobModel.updateOne(
-						{ _id: importJob._id },
-						{
-							$set: {
-								status: "success",
-								response: {
-									failed: response.failed,
-									successful: response.successful,
-									alreadyInDatabase: response.alreadyInDatabase,
-									successfulVideoIds: response.successfulVideoIds,
-									failedVideoIds: response.failedVideoIds
+					(importJob, response, next) => {
+						importJobModel.updateOne(
+							{ _id: importJob._id },
+							{
+								$set: {
+									status: "success",
+									response: {
+										failed: response.failed,
+										successful: response.successful,
+										alreadyInDatabase: response.alreadyInDatabase,
+										successfulVideoIds: response.successfulVideoIds,
+										failedVideoIds: response.failedVideoIds
+									}
 								}
+							},
+							err => {
+								if (err) next(err, importJob);
+								else
+									MediaModule.runJob("UPDATE_IMPORT_JOBS", { jobIds: importJob._id })
+										.then(() => next(null, importJob, response))
+										.catch(error => next(error, importJob));
 							}
-						},
-						err => {
-							if (err) next(err, importJob);
-							else
-								MediaModule.runJob("UPDATE_IMPORT_JOBS", { jobIds: importJob._id })
-									.then(() => next(null, importJob, response))
-									.catch(error => next(error, importJob));
-						}
-					);
-				}
-			],
-			async (err, importJob, response) => {
-				if (err) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+						);
+					}
+				],
+				async (err, importJob, response) => {
+					if (err) {
+						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+						this.log(
+							"ERROR",
+							"REQUEST_SET_ADMIN",
+							`Importing a YouTube playlist to be requested failed for admin "${session.userId}". "${err}"`
+						);
+						importJobModel.updateOne({ _id: importJob._id }, { $set: { status: "error" } });
+						MediaModule.runJob("UPDATE_IMPORT_JOBS", { jobIds: importJob._id });
+						return cb({ status: "error", message: err });
+					}
+
 					this.log(
-						"ERROR",
+						"SUCCESS",
 						"REQUEST_SET_ADMIN",
-						`Importing a YouTube playlist to be requested failed for admin "${session.userId}". "${err}"`
+						`Successfully imported a YouTube playlist to be requested for admin "${session.userId}".`
 					);
-					importJobModel.updateOne({ _id: importJob._id }, { $set: { status: "error" } });
-					MediaModule.runJob("UPDATE_IMPORT_JOBS", { jobIds: importJob._id });
-					return cb({ status: "error", message: err });
-				}
 
-				this.log(
-					"SUCCESS",
-					"REQUEST_SET_ADMIN",
-					`Successfully imported a YouTube playlist to be requested for admin "${session.userId}".`
-				);
+					this.publishProgress({
+						status: "success",
+						message: `Playlist is done importing. ${response.successful} were added succesfully, ${response.failed} failed (${response.alreadyInDatabase} were already in database)`
+					});
 
-				this.publishProgress({
-					status: "success",
-					message: `Playlist is done importing. ${response.successful} were added succesfully, ${response.failed} failed (${response.alreadyInDatabase} were already in database)`
-				});
-
-				return cb({
-					status: "success",
-					message: `Playlist is done importing. ${response.successful} were added succesfully, ${response.failed} failed (${response.alreadyInDatabase} were already in database)`,
-					videos: returnVideos ? response.videos : null
-				});
-			}
-		);
-	})
+					return cb({
+						status: "success",
+						message: `Playlist is done importing. ${response.successful} were added succesfully, ${response.failed} failed (${response.alreadyInDatabase} were already in database)`,
+						videos: returnVideos ? response.videos : null
+					});
+				}
+			);
+		}
+	)
 };

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно