فهرست منبع

feat: Add job context methods to get session user and assert logged in and perms

Owen Diffey 1 سال پیش
والد
کامیت
06bdd370da
2فایلهای تغییر یافته به همراه201 افزوده شده و 0 حذف شده
  1. 61 0
      backend/src/JobContext.ts
  2. 140 0
      backend/src/permissions.ts

+ 61 - 0
backend/src/JobContext.ts

@@ -1,3 +1,4 @@
+import { Types } from "mongoose";
 import BaseModule from "./BaseModule";
 import Job from "./Job";
 import JobQueue from "./JobQueue";
@@ -5,6 +6,9 @@ import { Log } from "./LogBook";
 import { SessionSchema } from "./schemas/session";
 import { JobOptions } from "./types/JobOptions";
 import { Jobs, Modules } from "./types/Modules";
+import { StationType } from "./schemas/station";
+import { UserRole, UserSchema } from "./schemas/user";
+import permissions from "./permissions";
 
 export default class JobContext {
 	public readonly job: Job;
@@ -13,6 +17,8 @@ export default class JobContext {
 
 	private session?: SessionSchema;
 
+	private user?: UserSchema;
+
 	public constructor(job: Job, session?: SessionSchema) {
 		this.job = job;
 		this.jobQueue = JobQueue.getPrimaryInstance();
@@ -66,4 +72,59 @@ export default class JobContext {
 			...(options ?? {})
 		}).execute();
 	}
+
+	public async getUser(refresh = false) {
+		if (!this.session?.userId) throw new Error("No user found for session");
+
+		if (this.user && !refresh) return this.user;
+
+		const User = await this.executeJob("data", "getModel", "user");
+
+		this.user = await User.findById(this.session.userId);
+
+		if (!this.user) throw new Error("No user found for session");
+
+		return this.user;
+	}
+
+	public async assertLoggedIn() {
+		if (!this.session?.userId) throw new Error("No user found for session");
+	}
+
+	public async assertPermission(
+		permission: string,
+		scope?: { stationId?: Types.ObjectId }
+	) {
+		if (!this.session?.userId) throw new Error("Insufficient permissions");
+
+		const user = await this.getUser();
+
+		const roles: (UserRole | "owner" | "dj")[] = [user.role];
+
+		if (scope?.stationId) {
+			const Station = await this.executeJob(
+				"data",
+				"getModel",
+				"station"
+			);
+
+			const station = await Station.findById(scope.stationId);
+
+			if (
+				station.type === StationType.COMMUNITY &&
+				station.owner === this.session.userId
+			)
+				roles.push("owner");
+			if (station.djs.find(dj => dj === this.session?.userId))
+				roles.push("dj");
+		}
+
+		let hasPermission;
+		roles.forEach(role => {
+			if (permissions[role] && permissions[role][permission])
+				hasPermission = true;
+		});
+
+		if (!hasPermission) throw new Error("Insufficient permissions");
+	}
 }

+ 140 - 0
backend/src/permissions.ts

@@ -0,0 +1,140 @@
+import config from "config";
+import { UserRole } from "./schemas/user";
+
+const user = {};
+
+const dj = {
+	"stations.autofill": true,
+	"stations.blacklist": true,
+	"stations.index": true,
+	"stations.playback.toggle": true,
+	"stations.queue.remove": true,
+	"stations.queue.reposition": true,
+	"stations.queue.reset": true,
+	"stations.request": true,
+	"stations.skip": true,
+	"stations.view": true,
+	"stations.view.manage": true
+};
+
+const owner = {
+	...dj,
+	"stations.djs.add": true,
+	"stations.djs.remove": true,
+	"stations.remove": true,
+	"stations.update": true
+};
+
+const moderator = {
+	...owner,
+	"admin.view": true,
+	"admin.view.import": true,
+	"admin.view.news": true,
+	"admin.view.playlists": true,
+	"admin.view.punishments": true,
+	"admin.view.reports": true,
+	"admin.view.songs": true,
+	"admin.view.stations": true,
+	"admin.view.users": true,
+	"admin.view.youtubeVideos": true,
+	"apis.searchDiscogs": !!config.get("apis.discogs.enabled"),
+	"news.create": true,
+	"news.update": true,
+	"playlists.create.admin": true,
+	"playlists.get": true,
+	"playlists.update.displayName": true,
+	"playlists.update.privacy": true,
+	"playlists.songs.add": true,
+	"playlists.songs.remove": true,
+	"playlists.songs.reposition": true,
+	"playlists.view.others": true,
+	"punishments.banIP": true,
+	"punishments.get": true,
+	"reports.get": true,
+	"reports.update": true,
+	"songs.create": true,
+	"songs.get": true,
+	"songs.update": true,
+	"songs.verify": true,
+	"stations.create.official": true,
+	"stations.index": false,
+	"stations.index.other": true,
+	"stations.remove": false,
+	"users.get": true,
+	"users.ban": true,
+	"users.requestPasswordReset": !!config.get("mail.enabled"),
+	"users.resendVerifyEmail": !!config.get("mail.enabled"),
+	"users.update": true,
+	"youtube.requestSetAdmin": true,
+	...(config.get("experimental.soundcloud")
+		? {
+				"admin.view.soundcloudTracks": true,
+				"admin.view.soundcloud": true,
+				"soundcloud.getArtist": true
+		  }
+		: {}),
+	...(config.get("experimental.spotify")
+		? {
+				"admin.view.spotify": true,
+				"spotify.getTracksFromMediaSources": true,
+				"spotify.getAlbumsFromIds": true,
+				"spotify.getArtistsFromIds": true,
+				"spotify.getAlternativeArtistSourcesForArtists": true,
+				"spotify.getAlternativeAlbumSourcesForAlbums": true,
+				"spotify.getAlternativeMediaSourcesForTracks": true,
+				"admin.view.youtubeChannels": true,
+				"youtube.getChannel": true
+		  }
+		: {})
+};
+
+const admin = {
+	...moderator,
+	"admin.view.dataRequests": true,
+	"admin.view.statistics": true,
+	"admin.view.youtube": true,
+	"dataRequests.resolve": true,
+	"media.recalculateAllRatings": true,
+	"media.removeImportJobs": true,
+	"news.remove": true,
+	"playlists.clearAndRefill": true,
+	"playlists.clearAndRefillAll": true,
+	"playlists.createMissing": true,
+	"playlists.deleteOrphaned": true,
+	"playlists.removeAdmin": true,
+	"playlists.requestOrphanedPlaylistSongs": true,
+	"punishments.deactivate": true,
+	"reports.remove": true,
+	"songs.remove": true,
+	"songs.updateAll": true,
+	"stations.clearEveryStationQueue": true,
+	"stations.remove": true,
+	"users.remove": true,
+	"users.remove.sessions": true,
+	"users.update.restricted": true,
+	"utils.getModules": true,
+	"youtube.getApiRequest": true,
+	"youtube.getMissingVideos": true,
+	"youtube.resetStoredApiRequests": true,
+	"youtube.removeStoredApiRequest": true,
+	"youtube.removeVideos": true,
+	"youtube.updateVideosV1ToV2": true,
+	...(config.get("experimental.soundcloud")
+		? {
+				"soundcloud.fetchNewApiKey": true,
+				"soundcloud.testApiKey": true
+		  }
+		: {}),
+	...(config.get("experimental.spotify")
+		? {
+				"youtube.getMissingChannels": true
+		  }
+		: {})
+};
+
+const permissions: Record<
+	UserRole | "owner" | "dj",
+	Record<string, boolean>
+> = { user, dj, owner, moderator, admin };
+
+export default permissions;