Kaynağa Gözat

refactor: Continued implementing hasPermission

Owen Diffey 1 yıl önce
ebeveyn
işleme
363cd94f16

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

@@ -200,9 +200,10 @@ export default {
 			page === "punishments" ||
 			page === "youtube" ||
 			page === "youtubeVideos" ||
-			page === "import"
+			page === "import" ||
+			page === "dataRequests"
 		) {
-			hasPermission(`apis.joinAdminRoom.${page}`, session.userId)
+			hasPermission(`admin.view.${page}`, session.userId)
 				.then(() =>
 					WSModule.runJob("SOCKET_LEAVE_ROOMS", { socketId: session.socketId }).then(() => {
 						WSModule.runJob(

+ 2 - 2
backend/logic/actions/dataRequests.js

@@ -19,7 +19,7 @@ CacheModule.runJob("SUB", {
 
 		dataRequestModel.findOne({ _id: dataRequestId }, (err, dataRequest) => {
 			WSModule.runJob("EMIT_TO_ROOM", {
-				room: "admin.users",
+				room: "admin.dataRequests",
 				args: ["event:admin.dataRequests.updated", { data: { dataRequest } }]
 			});
 		});
@@ -40,7 +40,7 @@ export default {
 	 * @param cb
 	 */
 	getData: useHasPermission(
-		"admin.view.users",
+		"admin.view.dataRequests",
 		async function getData(session, page, pageSize, properties, sort, queries, operator, cb) {
 			async.waterfall(
 				[

+ 50 - 47
backend/logic/actions/users.js

@@ -2447,61 +2447,64 @@ export default {
 	 * @param {string} newRole - the new role
 	 * @param {Function} cb - gets called with the result
 	 */
-	updateRole: useHasPermission("users.update", async function updateRole(session, updatingUserId, newRole, cb) {
-		newRole = newRole.toLowerCase();
-		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
+	updateRole: useHasPermission(
+		"users.update.restricted",
+		async function updateRole(session, updatingUserId, newRole, cb) {
+			newRole = newRole.toLowerCase();
+			const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
 
-		async.waterfall(
-			[
-				next => {
-					userModel.findOne({ _id: updatingUserId }, next);
-				},
+			async.waterfall(
+				[
+					next => {
+						userModel.findOne({ _id: updatingUserId }, next);
+					},
 
-				(user, next) => {
-					if (!user) return next("User not found.");
-					if (user.role === newRole) return next("New role can't be the same as the old role.");
-					return next();
-				},
-				next => {
-					userModel.updateOne(
-						{ _id: updatingUserId },
-						{ $set: { role: newRole } },
-						{ runValidators: true },
-						next
-					);
-				}
-			],
-			async err => {
-				if (err && err !== true) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+					(user, next) => {
+						if (!user) return next("User not found.");
+						if (user.role === newRole) return next("New role can't be the same as the old role.");
+						return next();
+					},
+					next => {
+						userModel.updateOne(
+							{ _id: updatingUserId },
+							{ $set: { role: newRole } },
+							{ runValidators: true },
+							next
+						);
+					}
+				],
+				async err => {
+					if (err && err !== true) {
+						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+
+						this.log(
+							"ERROR",
+							"UPDATE_ROLE",
+							`User "${session.userId}" couldn't update role for user "${updatingUserId}" to role "${newRole}". "${err}"`
+						);
+
+						return cb({ status: "error", message: err });
+					}
 
 					this.log(
-						"ERROR",
+						"SUCCESS",
 						"UPDATE_ROLE",
-						`User "${session.userId}" couldn't update role for user "${updatingUserId}" to role "${newRole}". "${err}"`
+						`User "${session.userId}" updated the role of user "${updatingUserId}" to role "${newRole}".`
 					);
 
-					return cb({ status: "error", message: err });
-				}
-
-				this.log(
-					"SUCCESS",
-					"UPDATE_ROLE",
-					`User "${session.userId}" updated the role of user "${updatingUserId}" to role "${newRole}".`
-				);
-
-				CacheModule.runJob("PUB", {
-					channel: "user.updated",
-					value: { userId: updatingUserId }
-				});
+					CacheModule.runJob("PUB", {
+						channel: "user.updated",
+						value: { userId: updatingUserId }
+					});
 
-				return cb({
-					status: "success",
-					message: "Role successfully updated."
-				});
-			}
-		);
-	}),
+					return cb({
+						status: "success",
+						message: "Role successfully updated."
+					});
+				}
+			);
+		}
+	),
 
 	/**
 	 * Updates a user's password

+ 8 - 6
backend/logic/hooks/hasPermission.js

@@ -32,6 +32,7 @@ permissions.moderator = {
 	"admin.view.reports": true,
 	"admin.view.songs": true,
 	"admin.view.stations": true,
+	"admin.view.users": true,
 	"admin.view.youtubeVideos": true,
 	"apis.searchDiscogs": true,
 	"news.create": true,
@@ -55,12 +56,17 @@ permissions.moderator = {
 	"stations.index": false,
 	"stations.index.other": true,
 	"stations.remove": false,
+	"users.get": true,
+	"users.ban": true,
+	"users.requestPasswordReset": true,
+	"users.resendVerifyEmail": true,
+	"users.update": true,
 	"youtube.requestSetAdmin": true
 };
 permissions.admin = {
 	...permissions.moderator,
+	"admin.view.dataRequests": true,
 	"admin.view.statistics": true,
-	"admin.view.users": true,
 	"admin.view.youtube": true,
 	"dataRequests.resolve": true,
 	"media.recalculateAllRatings": true,
@@ -78,13 +84,9 @@ permissions.admin = {
 	"songs.updateAll": true,
 	"stations.clearEveryStationQueue": true,
 	"stations.remove": true,
-	"users.get": true,
-	"users.update": true,
 	"users.remove": true,
 	"users.remove.sessions": true,
-	"users.requestPasswordReset": true,
-	"users.resendVerifyEmail": true,
-	"users.ban": true,
+	"users.update.restricted": true,
 	"utils.getModules": true,
 	"youtube.getApiRequest": true,
 	"youtube.resetStoredApiRequests": true,

+ 10 - 2
frontend/src/App.vue

@@ -1518,7 +1518,7 @@ button.delete:focus {
 			&.label {
 				border-radius: 0;
 			}
-			&:first-child {
+			&:first-child:not(:only-child) {
 				& > input,
 				& > select,
 				& > .button,
@@ -1526,7 +1526,7 @@ button.delete:focus {
 					border-radius: @border-radius 0 0 @border-radius;
 				}
 			}
-			&:last-child {
+			&:last-child:not(:only-child) {
 				& > input,
 				& > select,
 				& > .button,
@@ -1534,6 +1534,14 @@ button.delete:focus {
 					border-radius: 0 @border-radius @border-radius 0;
 				}
 			}
+			&:only-child {
+				& > input,
+				& > select,
+				& > .button,
+				&.label {
+					border-radius: @border-radius;
+				}
+			}
 		}
 	}
 

+ 18 - 3
frontend/src/components/PunishmentItem.vue

@@ -1,6 +1,7 @@
 <script setup lang="ts">
 import { defineAsyncComponent, computed } from "vue";
 import { format, formatDistance, parseISO } from "date-fns";
+import { useUserAuthStore } from "@/stores/userAuth";
 
 const UserLink = defineAsyncComponent(
 	() => import("@/components/UserLink.vue")
@@ -12,6 +13,8 @@ const props = defineProps({
 
 defineEmits(["deactivate"]);
 
+const { hasPermission } = useUserAuthStore();
+
 const active = computed(
 	() =>
 		props.punishment.active &&
@@ -23,19 +26,31 @@ const active = computed(
 	<div class="universal-item punishment-item">
 		<div class="item-icon">
 			<p class="is-expanded checkbox-control">
-				<label class="switch" :class="{ disabled: !active }">
+				<label
+					class="switch"
+					:class="{
+						disabled: !(
+							hasPermission('punishments.deactivate') && active
+						)
+					}"
+				>
 					<input
 						type="checkbox"
 						:checked="active"
 						@click="
-							active
+							hasPermission('punishments.deactivate') && active
 								? $emit('deactivate', $event)
 								: $event.preventDefault()
 						"
 					/>
 					<span
 						class="slider round"
-						:class="{ disabled: !active }"
+						:class="{
+							disabled: !(
+								hasPermission('punishments.deactivate') &&
+								active
+							)
+						}"
 					></span>
 				</label>
 			</p>

+ 4 - 0
frontend/src/components/modals/EditSong/index.vue

@@ -20,6 +20,7 @@ import { useWebsocketsStore } from "@/stores/websockets";
 import { useModalsStore } from "@/stores/modals";
 import { useEditSongStore } from "@/stores/editSong";
 import { useStationStore } from "@/stores/station";
+import { useUserAuthStore } from "@/stores/userAuth";
 
 import Modal from "@/components/Modal.vue";
 
@@ -55,10 +56,12 @@ const props = defineProps({
 const editSongStore = useEditSongStore(props);
 const stationStore = useStationStore();
 const { socket } = useWebsocketsStore();
+const userAuthStore = useUserAuthStore();
 
 const modalsStore = useModalsStore();
 const { modals, activeModals } = storeToRefs(modalsStore);
 const { openModal } = modalsStore;
+const { hasPermission } = userAuthStore;
 
 const {
 	tab,
@@ -2333,6 +2336,7 @@ onBeforeUnmount(() => {
 
 					<div class="right">
 						<button
+							v-if="hasPermission('songs.remove')"
 							class="button is-danger icon-with-button material-icons"
 							@click.prevent="
 								confirmAction({

+ 38 - 9
frontend/src/components/modals/EditUser.vue

@@ -13,6 +13,7 @@ import validation from "@/validation";
 import { useEditUserStore } from "@/stores/editUser";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useModalsStore } from "@/stores/modals";
+import { useUserAuthStore } from "@/stores/userAuth";
 
 const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
 const QuickConfirm = defineAsyncComponent(
@@ -32,6 +33,8 @@ const { setUser } = editUserStore;
 
 const { closeCurrentModal } = useModalsStore();
 
+const { hasPermission } = useUserAuthStore();
+
 const ban = ref({ reason: "", expiresAt: "1h" });
 
 const init = () => {
@@ -183,7 +186,10 @@ onBeforeUnmount(() => {
 								autofocus
 							/>
 						</span>
-						<span class="control">
+						<span
+							v-if="hasPermission('users.update')"
+							class="control"
+						>
 							<a class="button is-info" @click="updateUsername()"
 								>Update Username</a
 							>
@@ -201,7 +207,10 @@ onBeforeUnmount(() => {
 								autofocus
 							/>
 						</span>
-						<span class="control">
+						<span
+							v-if="hasPermission('users.update')"
+							class="control"
+						>
 							<a class="button is-info" @click="updateEmail()"
 								>Update Email Address</a
 							>
@@ -211,13 +220,21 @@ onBeforeUnmount(() => {
 					<label class="label"> Change user role </label>
 					<div class="control is-grouped">
 						<div class="control is-expanded select">
-							<select v-model="user.role">
+							<select
+								v-model="user.role"
+								:disabled="
+									!hasPermission('users.update.restricted')
+								"
+							>
 								<option>user</option>
 								<option>moderator</option>
 								<option>admin</option>
 							</select>
 						</div>
-						<p class="control">
+						<p
+							v-if="hasPermission('users.update.restricted')"
+							class="control"
+						>
 							<a class="button is-info" @click="updateRole()"
 								>Update Role</a
 							>
@@ -225,7 +242,7 @@ onBeforeUnmount(() => {
 					</div>
 				</div>
 
-				<div class="section">
+				<div v-if="hasPermission('users.ban')" class="section">
 					<label class="label"> Punish/Ban User </label>
 					<p class="control is-grouped">
 						<span class="control select">
@@ -258,16 +275,28 @@ onBeforeUnmount(() => {
 				</div>
 			</template>
 			<template #footer>
-				<quick-confirm @confirm="resendVerificationEmail()">
+				<quick-confirm
+					v-if="hasPermission('users.resendVerifyEmail')"
+					@confirm="resendVerificationEmail()"
+				>
 					<a class="button is-warning"> Resend verification email </a>
 				</quick-confirm>
-				<quick-confirm @confirm="requestPasswordReset()">
+				<quick-confirm
+					v-if="hasPermission('users.requestPasswordReset')"
+					@confirm="requestPasswordReset()"
+				>
 					<a class="button is-warning"> Request password reset </a>
 				</quick-confirm>
-				<quick-confirm @confirm="removeSessions()">
+				<quick-confirm
+					v-if="hasPermission('users.remove.sessions')"
+					@confirm="removeSessions()"
+				>
 					<a class="button is-warning"> Remove all sessions </a>
 				</quick-confirm>
-				<quick-confirm @confirm="removeAccount()">
+				<quick-confirm
+					v-if="hasPermission('users.remove')"
+					@confirm="removeAccount()"
+				>
 					<a class="button is-danger"> Remove account </a>
 				</quick-confirm>
 			</template>

+ 10 - 10
frontend/src/components/modals/ManageStation/index.vue

@@ -394,20 +394,20 @@ onBeforeUnmount(() => {
 	<modal
 		v-if="station"
 		:title="
-			sector === 'home' && !hasPermission('stations.update')
+			sector === 'home' && !hasPermission('stations.view.manage')
 				? 'View Queue'
-				: !hasPermission('stations.update')
+				: !hasPermission('stations.view.manage')
 				? 'Add Song to Queue'
 				: 'Manage Station'
 		"
 		:style="`--primary-color: var(--${station.theme})`"
 		class="manage-station-modal"
 		:size="
-			hasPermission('stations.update') || sector !== 'home'
+			hasPermission('stations.view.manage') || sector !== 'home'
 				? 'wide'
 				: null
 		"
-		:split="hasPermission('stations.update') || sector !== 'home'"
+		:split="hasPermission('stations.view.manage') || sector !== 'home'"
 	>
 		<template #body v-if="station && station._id">
 			<div class="left-section">
@@ -423,7 +423,7 @@ onBeforeUnmount(() => {
 					</div>
 					<div
 						v-if="
-							hasPermission('stations.update') ||
+							hasPermission('stations.view.manage') ||
 							sector !== 'home'
 						"
 					>
@@ -448,7 +448,7 @@ onBeforeUnmount(() => {
 							</button>
 							<button
 								v-if="
-									hasPermission('stations.view.manage') &&
+									hasPermission('stations.autofill') &&
 									station.autofill.enabled
 								"
 								class="button is-default"
@@ -459,7 +459,7 @@ onBeforeUnmount(() => {
 								Autofill
 							</button>
 							<button
-								v-if="hasPermission('stations.view.manage')"
+								v-if="hasPermission('stations.blacklist')"
 								class="button is-default"
 								:class="{ selected: tab === 'blacklist' }"
 								:ref="el => (tabs['blacklist-tab'] = el)"
@@ -485,7 +485,7 @@ onBeforeUnmount(() => {
 						/>
 						<playlist-tab-base
 							v-if="
-								hasPermission('stations.view.manage') &&
+								hasPermission('stations.autofill') &&
 								station.autofill.enabled
 							"
 							class="tab"
@@ -501,7 +501,7 @@ onBeforeUnmount(() => {
 							</template>
 						</playlist-tab-base>
 						<playlist-tab-base
-							v-if="hasPermission('stations.view.manage')"
+							v-if="hasPermission('stations.blacklist')"
 							class="tab"
 							v-show="tab === 'blacklist'"
 							:type="'blacklist'"
@@ -543,7 +543,7 @@ onBeforeUnmount(() => {
 					<a class="button is-danger">Reset queue</a>
 				</quick-confirm>
 				<quick-confirm
-					v-if="hasPermission('stations.queue.reset')"
+					v-if="hasPermission('stations.remove')"
 					@confirm="removeStation()"
 				>
 					<button class="button is-danger">Delete station</button>

+ 19 - 5
frontend/src/components/modals/ViewReport.vue

@@ -5,6 +5,7 @@ import { storeToRefs } from "pinia";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useModalsStore } from "@/stores/modals";
 import { useViewReportStore } from "@/stores/viewReport";
+import { useUserAuthStore } from "@/stores/userAuth";
 import { useReports } from "@/composables/useReports";
 import ws from "@/ws";
 import { Report } from "@/types/report";
@@ -33,6 +34,9 @@ const { openModal, closeCurrentModal } = useModalsStore();
 
 const { resolveReport, removeReport } = useReports();
 
+const userAuthStore = useUserAuthStore();
+const { hasPermission } = userAuthStore;
+
 const icons = ref({
 	duration: "timer",
 	video: "tv",
@@ -188,7 +192,10 @@ onBeforeUnmount(() => {
 								class="material-icons resolve-icon"
 								content="Resolve"
 								v-tippy
-								v-if="!issue.resolved"
+								v-if="
+									!issue.resolved &&
+									hasPermission('reports.update')
+								"
 								@click="toggleIssue(issue._id)"
 							>
 								done
@@ -197,7 +204,10 @@ onBeforeUnmount(() => {
 								class="material-icons unresolve-icon"
 								content="Unresolve"
 								v-tippy
-								v-else
+								v-else-if="
+									issue.resolved &&
+									hasPermission('reports.update')
+								"
 								@click="toggleIssue(issue._id)"
 							>
 								remove
@@ -209,6 +219,7 @@ onBeforeUnmount(() => {
 		</template>
 		<template #footer v-if="report && report._id">
 			<a
+				v-if="hasPermission('songs.update')"
 				class="button is-primary material-icons icon-with-button"
 				@click="openSong()"
 				content="Edit Song"
@@ -217,7 +228,7 @@ onBeforeUnmount(() => {
 				edit
 			</a>
 			<button
-				v-if="report.resolved"
+				v-if="report.resolved && hasPermission('reports.update')"
 				class="button is-danger material-icons icon-with-button"
 				@click="resolve(false)"
 				content="Unresolve"
@@ -226,7 +237,7 @@ onBeforeUnmount(() => {
 				remove_done
 			</button>
 			<button
-				v-else
+				v-else-if="!report.resolved && hasPermission('reports.update')"
 				class="button is-success material-icons icon-with-button"
 				@click="resolve(true)"
 				content="Resolve"
@@ -235,7 +246,10 @@ onBeforeUnmount(() => {
 				done_all
 			</button>
 			<div class="right">
-				<quick-confirm @confirm="remove()">
+				<quick-confirm
+					v-if="hasPermission('reports.remove')"
+					@confirm="remove()"
+				>
 					<button
 						class="button is-danger material-icons icon-with-button"
 						content="Delete Report"

+ 9 - 0
frontend/src/components/modals/ViewYoutubeVideo.vue

@@ -8,6 +8,7 @@ import { useWebsocketsStore } from "@/stores/websockets";
 import { useModalsStore } from "@/stores/modals";
 import { useViewYoutubeVideoStore } from "@/stores/viewYoutubeVideo";
 import { useStationStore } from "@/stores/station";
+import { useUserAuthStore } from "@/stores/userAuth";
 
 import Modal from "@/components/Modal.vue";
 
@@ -47,6 +48,9 @@ const { openModal, closeCurrentModal } = useModalsStore();
 
 const { socket } = useWebsocketsStore();
 
+const userAuthStore = useUserAuthStore();
+const { hasPermission } = userAuthStore;
+
 const remove = () => {
 	socket.dispatch("youtube.removeVideos", videoId.value, res => {
 		if (res.status === "success") {
@@ -693,6 +697,10 @@ onBeforeUnmount(() => {
 		</template>
 		<template #footer>
 			<button
+				v-if="
+					hasPermission('songs.create') ||
+					hasPermission('songs.update')
+				"
 				class="button is-primary icon-with-button material-icons"
 				@click.prevent="
 					openModal({ modal: 'editSong', data: { song: video } })
@@ -704,6 +712,7 @@ onBeforeUnmount(() => {
 			</button>
 			<div class="right">
 				<button
+					v-if="hasPermission('youtube.removeVideos')"
 					class="button is-danger icon-with-button material-icons"
 					@click.prevent="
 						confirmAction({

+ 1 - 1
frontend/src/main.ts

@@ -196,7 +196,7 @@ const router = createRouter({
 					path: "users/data-requests",
 					component: () =>
 						import("@/pages/Admin/Users/DataRequests.vue"),
-					meta: { permissionRequired: "admin.view.users" }
+					meta: { permissionRequired: "admin.view.dataRequests" }
 				},
 				{
 					path: "users/punishments",

+ 6 - 0
frontend/src/pages/Admin/News.vue

@@ -3,6 +3,7 @@ import { defineAsyncComponent, ref } from "vue";
 import Toast from "toasters";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useModalsStore } from "@/stores/modals";
+import { useUserAuthStore } from "@/stores/userAuth";
 import { TableColumn, TableFilter, TableEvents } from "@/types/advancedTable";
 
 const AdvancedTable = defineAsyncComponent(
@@ -123,6 +124,8 @@ const events = ref(<TableEvents>{
 
 const { openModal } = useModalsStore();
 
+const { hasPermission } = useUserAuthStore();
+
 const remove = id => {
 	socket.dispatch("news.remove", id, res => new Toast(res.message));
 };
@@ -138,6 +141,7 @@ const remove = id => {
 			</div>
 			<div class="button-row">
 				<button
+					v-if="hasPermission('news.create')"
 					class="is-primary button"
 					@click="
 						openModal({
@@ -162,6 +166,7 @@ const remove = id => {
 			<template #column-options="slotProps">
 				<div class="row-options">
 					<button
+						v-if="hasPermission('news.update')"
 						class="button is-primary icon-with-button material-icons"
 						@click="
 							openModal({
@@ -175,6 +180,7 @@ const remove = id => {
 						edit
 					</button>
 					<quick-confirm
+						v-if="hasPermission('news.remove')"
 						@confirm="remove(slotProps.item._id)"
 						:disabled="slotProps.item.removed"
 					>

+ 22 - 14
frontend/src/pages/Admin/Playlists.vue

@@ -1,6 +1,7 @@
 <script setup lang="ts">
 import { defineAsyncComponent, ref } from "vue";
 import { useModalsStore } from "@/stores/modals";
+import { useUserAuthStore } from "@/stores/userAuth";
 import utils from "@/utils";
 import { TableColumn, TableFilter, TableEvents } from "@/types/advancedTable";
 
@@ -14,6 +15,8 @@ const UserLink = defineAsyncComponent(
 	() => import("@/components/UserLink.vue")
 );
 
+const { hasPermission } = useUserAuthStore();
+
 const columnDefault = ref(<TableColumn>{
 	sortable: true,
 	hidable: true,
@@ -199,32 +202,37 @@ const events = ref(<TableEvents>{
 		id: "playlistId"
 	}
 });
-const jobs = ref([
-	{
+const jobs = ref([]);
+if (hasPermission("playlists.deleteOrphaned")) {
+	jobs.value.push({
 		name: "Delete orphaned station playlists",
 		socket: "playlists.deleteOrphanedStationPlaylists"
-	},
-	{
+	});
+	jobs.value.push({
 		name: "Delete orphaned genre playlists",
 		socket: "playlists.deleteOrphanedGenrePlaylists"
-	},
-	{
+	});
+}
+if (hasPermission("playlists.requestOrphanedPlaylistSongs"))
+	jobs.value.push({
 		name: "Request orphaned playlist songs",
 		socket: "playlists.requestOrphanedPlaylistSongs"
-	},
-	{
+	});
+if (hasPermission("playlists.clearAndRefillAll")) {
+	jobs.value.push({
 		name: "Clear and refill all station playlists",
 		socket: "playlists.clearAndRefillAllStationPlaylists"
-	},
-	{
+	});
+	jobs.value.push({
 		name: "Clear and refill all genre playlists",
 		socket: "playlists.clearAndRefillAllGenrePlaylists"
-	},
-	{
+	});
+}
+if (hasPermission("playlists.createMissing"))
+	jobs.value.push({
 		name: "Create missing genre playlists",
 		socket: "playlists.createMissingGenrePlaylists"
-	}
-]);
+	});
 
 const { openModal } = useModalsStore();
 

+ 12 - 2
frontend/src/pages/Admin/Reports.vue

@@ -2,6 +2,7 @@
 import { defineAsyncComponent, ref } from "vue";
 import Toast from "toasters";
 import { useModalsStore } from "@/stores/modals";
+import { useUserAuthStore } from "@/stores/userAuth";
 import { useReports } from "@/composables/useReports";
 import { TableColumn, TableFilter, TableEvents } from "@/types/advancedTable";
 
@@ -151,6 +152,9 @@ const { openModal } = useModalsStore();
 
 const { resolveReport } = useReports();
 
+const userAuthStore = useUserAuthStore();
+const { hasPermission } = userAuthStore;
+
 const resolve = (reportId, value) =>
 	resolveReport({ reportId, value })
 		.then((res: any) => {
@@ -204,7 +208,10 @@ const getDateFormatted = createdAt => {
 						open_in_full
 					</button>
 					<button
-						v-if="slotProps.item.resolved"
+						v-if="
+							slotProps.item.resolved &&
+							hasPermission('reports.update')
+						"
 						class="button is-danger material-icons icon-with-button"
 						@click="resolve(slotProps.item._id, false)"
 						:disabled="slotProps.item.removed"
@@ -214,7 +221,10 @@ const getDateFormatted = createdAt => {
 						remove_done
 					</button>
 					<button
-						v-else
+						v-else-if="
+							!slotProps.item.resolved &&
+							hasPermission('reports.update')
+						"
 						class="button is-success material-icons icon-with-button"
 						@click="resolve(slotProps.item._id, true)"
 						:disabled="slotProps.item.removed"

+ 21 - 3
frontend/src/pages/Admin/Songs/index.vue

@@ -49,8 +49,18 @@ const columns = ref(<TableColumn[]>[
 		sortable: false,
 		hidable: false,
 		resizable: false,
-		minWidth: 129,
-		defaultWidth: 129
+		minWidth:
+			hasPermission("songs.verify") &&
+			hasPermission("songs.update") &&
+			hasPermission("songs.remove")
+				? 129
+				: 85,
+		defaultWidth:
+			hasPermission("songs.verify") &&
+			hasPermission("songs.update") &&
+			hasPermission("songs.remove")
+				? 129
+				: 85
 	},
 	{
 		name: "thumbnailImage",
@@ -538,10 +548,18 @@ onMounted(() => {
 				<p>Create, edit and manage songs in the catalogue</p>
 			</div>
 			<div class="button-row">
-				<button class="button is-primary" @click="create()">
+				<button
+					v-if="hasPermission('songs.create')"
+					class="button is-primary"
+					@click="create()"
+				>
 					Create song
 				</button>
 				<button
+					v-if="
+						hasPermission('songs.create') ||
+						hasPermission('songs.update')
+					"
 					class="button is-primary"
 					@click="openModal('importAlbum')"
 				>

+ 11 - 6
frontend/src/pages/Admin/Stations.vue

@@ -3,6 +3,7 @@ import { defineAsyncComponent, ref } from "vue";
 import Toast from "toasters";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useModalsStore } from "@/stores/modals";
+import { useUserAuthStore } from "@/stores/userAuth";
 import { TableColumn, TableFilter, TableEvents } from "@/types/advancedTable";
 
 const AdvancedTable = defineAsyncComponent(
@@ -20,6 +21,8 @@ const UserLink = defineAsyncComponent(
 
 const { socket } = useWebsocketsStore();
 
+const { hasPermission } = useUserAuthStore();
+
 const columnDefault = ref(<TableColumn>{
 	sortable: true,
 	hidable: true,
@@ -37,8 +40,8 @@ const columns = ref(<TableColumn[]>[
 		sortable: false,
 		hidable: false,
 		resizable: false,
-		minWidth: 129,
-		defaultWidth: 129
+		minWidth: hasPermission("stations.remove") ? 129 : 85,
+		defaultWidth: hasPermission("stations.remove") ? 129 : 85
 	},
 	{
 		name: "_id",
@@ -296,12 +299,12 @@ const events = ref(<TableEvents>{
 		id: "stationId"
 	}
 });
-const jobs = ref([
-	{
+const jobs = ref([]);
+if (hasPermission("stations.clearEveryStationQueue"))
+	jobs.value.push({
 		name: "Clear every station queue",
 		socket: "stations.clearEveryStationQueue"
-	}
-]);
+	});
 
 const { openModal } = useModalsStore();
 
@@ -324,6 +327,7 @@ const remove = stationId => {
 			</div>
 			<div class="button-row">
 				<button
+					v-if="hasPermission('stations.create.official')"
 					class="button is-primary"
 					@click="
 						openModal({
@@ -365,6 +369,7 @@ const remove = stationId => {
 						settings
 					</button>
 					<quick-confirm
+						v-if="hasPermission('stations.remove')"
 						@confirm="remove(slotProps.item._id)"
 						:disabled="slotProps.item.removed"
 					>

+ 1 - 1
frontend/src/pages/Admin/Users/DataRequests.vue

@@ -81,7 +81,7 @@ const filters = ref(<TableFilter[]>[
 	}
 ]);
 const events = ref(<TableEvents>{
-	adminRoom: "users",
+	adminRoom: "dataRequests",
 	updated: {
 		event: "admin.dataRequests.updated",
 		id: "dataRequest._id",

+ 5 - 1
frontend/src/pages/Admin/Users/Punishments.vue

@@ -3,6 +3,7 @@ import { defineAsyncComponent, ref } from "vue";
 import Toast from "toasters";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useModalsStore } from "@/stores/modals";
+import { useUserAuthStore } from "@/stores/userAuth";
 import { TableColumn, TableFilter, TableEvents } from "@/types/advancedTable";
 
 const AdvancedTable = defineAsyncComponent(
@@ -163,6 +164,8 @@ const events = ref(<TableEvents>{
 
 const { openModal } = useModalsStore();
 
+const { hasPermission } = useUserAuthStore();
+
 const banIP = () => {
 	socket.dispatch(
 		"punishments.banIP",
@@ -231,6 +234,7 @@ const deactivatePunishment = punishmentId => {
 						open_in_full
 					</button>
 					<quick-confirm
+						v-if="hasPermission('punishments.deactivate')"
 						@confirm="deactivatePunishment(slotProps.item._id)"
 						:disabled="
 							slotProps.item.status === 'Inactive' ||
@@ -302,7 +306,7 @@ const deactivatePunishment = punishmentId => {
 				}}</span>
 			</template>
 		</advanced-table>
-		<div class="card">
+		<div v-if="hasPermission('punishments.banIP')" class="card">
 			<h4>Ban an IP</h4>
 			<hr class="section-horizontal-rule" />
 			<div class="card-content">

+ 32 - 6
frontend/src/pages/Admin/YouTube/Videos.vue

@@ -4,6 +4,7 @@ import Toast from "toasters";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useLongJobsStore } from "@/stores/longJobs";
 import { useModalsStore } from "@/stores/modals";
+import { useUserAuthStore } from "@/stores/userAuth";
 import {
 	TableColumn,
 	TableFilter,
@@ -25,6 +26,9 @@ const { setJob } = useLongJobsStore();
 
 const { socket } = useWebsocketsStore();
 
+const userAuthStore = useUserAuthStore();
+const { hasPermission } = userAuthStore;
+
 const columnDefault = ref(<TableColumn>{
 	sortable: true,
 	hidable: true,
@@ -42,8 +46,16 @@ const columns = ref(<TableColumn[]>[
 		sortable: false,
 		hidable: false,
 		resizable: false,
-		minWidth: 129,
-		defaultWidth: 129
+		minWidth:
+			(hasPermission("songs.create") || hasPermission("songs.update")) &&
+			hasPermission("youtube.removeVideos")
+				? 129
+				: 85,
+		defaultWidth:
+			(hasPermission("songs.create") || hasPermission("songs.update")) &&
+			hasPermission("youtube.removeVideos")
+				? 129
+				: 85
 	},
 	{
 		name: "thumbnailImage",
@@ -184,12 +196,12 @@ const events = ref(<TableEvents>{
 	}
 });
 const bulkActions = ref(<TableBulkActions>{ width: 200 });
-const jobs = ref([
-	{
+const jobs = ref([]);
+if (hasPermission("media.recalculateAllRatings"))
+	jobs.value.push({
 		name: "Recalculate all ratings",
 		socket: "media.recalculateAllRatings"
-	}
-]);
+	});
 
 const { openModal } = useModalsStore();
 
@@ -315,6 +327,10 @@ const confirmAction = ({ message, action, params }) => {
 						open_in_full
 					</button>
 					<button
+						v-if="
+							hasPermission('songs.create') ||
+							hasPermission('songs.update')
+						"
 						class="button is-primary icon-with-button material-icons"
 						@click="editOne(slotProps.item)"
 						:disabled="slotProps.item.removed"
@@ -328,6 +344,7 @@ const confirmAction = ({ message, action, params }) => {
 						music_note
 					</button>
 					<button
+						v-if="hasPermission('youtube.removeVideos')"
 						class="button is-danger icon-with-button material-icons"
 						@click.prevent="
 							confirmAction({
@@ -392,6 +409,10 @@ const confirmAction = ({ message, action, params }) => {
 			<template #bulk-actions="slotProps">
 				<div class="bulk-actions">
 					<i
+						v-if="
+							hasPermission('songs.create') ||
+							hasPermission('songs.update')
+						"
 						class="material-icons create-songs-icon"
 						@click.prevent="editMany(slotProps.item)"
 						content="Create/edit songs from videos"
@@ -401,6 +422,10 @@ const confirmAction = ({ message, action, params }) => {
 						music_note
 					</i>
 					<i
+						v-if="
+							hasPermission('songs.create') ||
+							hasPermission('songs.update')
+						"
 						class="material-icons import-album-icon"
 						@click.prevent="importAlbum(slotProps.item)"
 						content="Import album from videos"
@@ -410,6 +435,7 @@ const confirmAction = ({ message, action, params }) => {
 						album
 					</i>
 					<i
+						v-if="hasPermission('youtube.removeVideos')"
 						class="material-icons delete-icon"
 						@click.prevent="
 							confirmAction({

+ 11 - 1
frontend/src/pages/Admin/index.vue

@@ -230,7 +230,7 @@ onBeforeUnmount(() => {
 							</div>
 							<router-link
 								v-else-if="
-									hasPermission('admin.view.users') &&
+									hasPermission('admin.view.songs') &&
 									!sidebarActive
 								"
 								class="sidebar-item songs"
@@ -317,12 +317,22 @@ onBeforeUnmount(() => {
 										Users
 									</router-link>
 									<router-link
+										v-if="
+											hasPermission(
+												'admin.view.dataRequests'
+											)
+										"
 										class="sidebar-item-child"
 										to="/admin/users/data-requests"
 									>
 										Data Requests
 									</router-link>
 									<router-link
+										v-if="
+											hasPermission(
+												'admin.view.punishments'
+											)
+										"
 										class="sidebar-item-child"
 										to="/admin/users/punishments"
 									>