2 Commits 5e845faa98 ... d13cae1249

Author SHA1 Message Date
  Kristian Vos d13cae1249 refactor: further worked on the event system, with permissions for subscribing 1 month ago
  Kristian Vos d85e1f5c99 refactor: fix TS issues for events 1 month ago

+ 8 - 4
backend/src/@types/mongoose.d.ts

@@ -1,3 +1,7 @@
+import ModelCreatedEvent from "@/modules/DataModule/ModelCreatedEvent";
+import ModelDeletedEvent from "@/modules/DataModule/ModelDeletedEvent";
+import ModelUpdatedEvent from "@/modules/DataModule/ModelUpdatedEvent";
+
 declare module "mongoose" {
 	// Add some additional possible config options to Mongoose's schema options
 	interface SchemaOptions<
@@ -7,12 +11,12 @@ declare module "mongoose" {
 		QueryHelpers = {},
 		TStaticMethods = {},
 		TVirtuals = {},
-		/* eslint-enable */
 		THydratedDocumentType = HydratedDocument<
 			DocType,
 			TInstanceMethods,
 			QueryHelpers
 		>
+		/* eslint-enable */
 	> {
 		patchHistory?: {
 			enabled: boolean;
@@ -40,13 +44,13 @@ declare module "mongoose" {
 
 		eventListeners?: {
 			[key: `${string}.created`]: (
-				doc: THydratedDocumentType
+				event: ModelCreatedEvent
 			) => Promise<void>;
 			[key: `${string}.updated`]: (
-				doc: THydratedDocumentType
+				event: ModelUpdatedEvent
 			) => Promise<void>;
 			[key: `${string}.deleted`]: (
-				oldDoc: THydratedDocumentType
+				event: ModelDeletedEvent
 			) => Promise<void>;
 		};
 	}

+ 3 - 3
backend/src/BaseModule.ts

@@ -4,7 +4,7 @@ import { forEachIn } from "@common/utils/forEachIn";
 import LogBook, { Log } from "@/LogBook";
 import ModuleManager from "@/ModuleManager";
 import Job from "./Job";
-import Event from "./modules/EventsModule/Event";
+import { EventClass } from "./modules/EventsModule/Event";
 
 export enum ModuleStatus {
 	LOADED = "LOADED",
@@ -25,7 +25,7 @@ export default abstract class BaseModule {
 
 	protected _jobs: Record<string, typeof Job>;
 
-	protected _events: Record<string, typeof Event>;
+	protected _events: Record<string, EventClass>;
 
 	/**
 	 * Base Module
@@ -182,7 +182,7 @@ export default abstract class BaseModule {
 	/**
 	 * getEvent - Get module event
 	 */
-	public getEvent(name: string) {
+	public getEvent(name: string): EventClass {
 		const [, Event] =
 			Object.entries(this._events).find(
 				([eventName]) => eventName === name

+ 0 - 4
backend/src/modules/DataModule/DataModuleEvent.ts

@@ -13,8 +13,4 @@ export default abstract class DataModuleEvent extends ModuleEvent {
 	public static getModelName() {
 		return this._modelName;
 	}
-
-	public getModelName() {
-		return (this.constructor as typeof DataModuleEvent).getModelName();
-	}
 }

+ 3 - 1
backend/src/modules/DataModule/models/users/permissions.ts

@@ -174,7 +174,7 @@ const admin = {
 	// Frontend admin views
 	"admin.view.dataRequests": true,
 	"admin.view.statistics": true,
-	"admin.view.youtube": true
+	"admin.view.youtube": true,
 
 	// // Experimental SoundCloud
 	// ...(config.get("experimental.soundcloud")
@@ -190,6 +190,8 @@ const admin = {
 	// 			"youtube.getMissingChannels": true
 	// 	  }
 	// 	: {})
+
+	"event.model.news.created": true // WIP - regular users need to be able to subscribe to certain news subscribe events
 };
 
 const permissions: Record<

+ 4 - 6
backend/src/modules/EventsModule.ts

@@ -12,8 +12,6 @@ import { forEachIn } from "@common/utils/forEachIn";
 import BaseModule, { ModuleStatus } from "@/BaseModule";
 import WebSocketModule from "./WebSocketModule";
 import Event from "@/modules/EventsModule/Event";
-import { EventDerived } from "@/types/EventDerived";
-import assertEventDerived from "@/utils/assertEventDerived";
 import ModuleManager from "@/ModuleManager";
 
 export class EventsModule extends BaseModule {
@@ -33,7 +31,7 @@ export class EventsModule extends BaseModule {
 
 	private _pSubscriptions: Record<
 		string,
-		((message: string, key: string) => Promise<void>)[]
+		((event: Event) => Promise<void>)[]
 	>;
 
 	private _socketSubscriptions: Record<string, Set<string>>;
@@ -210,7 +208,7 @@ export class EventsModule extends BaseModule {
 	/**
 	 * publish - Publish an event
 	 */
-	public async publish(event: typeof Event) {
+	public async publish(event: Event) {
 		if (!this._pubClient) throw new Error("Redis pubClient unavailable.");
 
 		const channel = event.getKey();
@@ -231,7 +229,7 @@ export class EventsModule extends BaseModule {
 
 		const { path, scope } = Event.parseKey(key);
 		const EventClass = this.getEvent(path);
-		const parsedMessage = EventClass.parseMessage(message);
+		const parsedMessage = Event.parseMessage(message);
 		const event = new EventClass(parsedMessage, scope);
 
 		if (this._subscriptions && this._subscriptions[key])
@@ -285,7 +283,7 @@ export class EventsModule extends BaseModule {
 	 */
 	public async pSubscribe(
 		pattern: string,
-		callback: (message: string, key: string) => Promise<void>
+		callback: (event: Event) => Promise<void>
 	) {
 		if (!this._subClient) throw new Error("Redis subClient unavailable.");
 

+ 5 - 1
backend/src/modules/EventsModule/Event.ts

@@ -1,4 +1,4 @@
-import { HydratedDocument, Model } from "mongoose";
+import { HydratedDocument } from "mongoose";
 import { UserSchema } from "@models/users/schema";
 
 export default abstract class Event {
@@ -122,3 +122,7 @@ export default abstract class Event {
 		return (this.constructor as typeof Event).makeMessage(this._data);
 	}
 }
+
+export type EventClass = {
+	new (...params: ConstructorParameters<typeof Event>): Event;
+};

+ 20 - 9
backend/src/modules/EventsModule/jobs/Subscribe.ts

@@ -1,6 +1,9 @@
 import Job, { JobOptions } from "@/Job";
 import EventsModule from "@/modules/EventsModule";
 
+const channelRegex =
+	/^(?<moduleName>[a-z]+)\.(?<modelName>[A-z]+)\.(?<event>[A-z]+):?(?<modelId>[A-z0-9]+)?$/;
+
 export default class Subscribe extends Job {
 	protected static _hasPermission = true;
 
@@ -16,18 +19,26 @@ export default class Subscribe extends Job {
 			throw new Error("Channel must be a string");
 	}
 
-	protected override async _authorize() {}
+	protected override async _authorize() {
+		const { channel } = this._payload;
+
+		const { moduleName, modelName, event, modelId } =
+			channelRegex.exec(channel)?.groups ?? {};
 
-	// protected override async _authorize() {
-	// const [path, scope] = this._payload.channel.split(":");
+		let permission = `event.${channel}`;
 
-	// const EventClass = EventsModule.getEvent(path);
+		if (
+			moduleName === "data" &&
+			modelName &&
+			(modelId || event === "created")
+		) {
+			if (event === "created")
+				permission = `event.model.${modelName}.created`;
+			else permission = `data.${modelName}.findById.${modelId}`;
+		}
 
-	// const hasPermission = EventClass.hasPermission(
-	// 	this._context.getUser(),
-	// 	scope
-	// );
-	// }
+		await this._context.assertPermission(permission);
+	}
 
 	protected async _execute() {
 		const socketId = this._context.getSocketId();

+ 19 - 21
backend/src/modules/EventsModule/jobs/SubscribeMany.ts

@@ -2,7 +2,7 @@ import Job, { JobOptions } from "@/Job";
 import EventsModule from "@/modules/EventsModule";
 
 const channelRegex =
-	/^(?<moduleName>[a-z]+)\.(?<modelName>[A-z]+)\.(?<event>[A-z]+)\.?(?<modelId>[A-z0-9]+)?$/;
+	/^(?<moduleName>[a-z]+)\.(?<modelName>[A-z]+)\.(?<event>[A-z]+):?(?<modelId>[A-z0-9]+)?$/;
 
 export default class SubscribeMany extends Job {
 	protected static _hasPermission = true;
@@ -24,30 +24,28 @@ export default class SubscribeMany extends Job {
 		});
 	}
 
-	protected override async _authorize() {}
+	protected override async _authorize() {
+		const permissions = this._payload.channels.map((channel: string) => {
+			const { moduleName, modelName, event, modelId } =
+				channelRegex.exec(channel)?.groups ?? {};
 
-	// protected override async _authorize() {
-	// const permissions = this._payload.channels.map((channel: string) => {
-	// 	const { moduleName, modelName, event, modelId } =
-	// 		channelRegex.exec(channel)?.groups ?? {};
+			let permission = `event.${channel}`;
 
-	// 	let permission = `event.${channel}`;
+			if (
+				moduleName === "data" &&
+				modelName &&
+				(modelId || event === "created")
+			) {
+				if (event === "created")
+					permission = `event.model.${modelName}.created`;
+				else permission = `data.${modelName}.findById.${modelId}`;
+			}
 
-	// 	if (
-	// 		moduleName === "model" &&
-	// 		modelName &&
-	// 		(modelId || event === "created")
-	// 	) {
-	// 		if (event === "created")
-	// 			permission = `event.model.${modelName}.created`;
-	// 		else permission = `data.${modelName}.findById.${modelId}`;
-	// 	}
-
-	// 	return permission;
-	// });
+			return permission;
+		});
 
-	// await this._context.assertPermissions(permissions);
-	// }
+		await this._context.assertPermissions(permissions);
+	}
 
 	protected async _execute() {
 		const socketId = this._context.getSocketId();

+ 1 - 1
backend/src/modules/WebSocketModule.ts

@@ -13,7 +13,7 @@ import DataModule from "./DataModule";
 import { UserModel } from "./DataModule/models/users/schema";
 import { SessionModel } from "./DataModule/models/sessions/schema";
 import EventsModule from "./EventsModule";
-import assertEventDerived from "@/utils/assertEventDerived";
+// import assertEventDerived from "@/utils/assertEventDerived";
 
 export class WebSocketModule extends BaseModule {
 	private _httpServer?: Server;

+ 1 - 1
backend/src/utils/assertEventDerived.ts

@@ -1,7 +1,7 @@
 import Event from "@/modules/EventsModule/Event";
 
 // eslint-disable-next-line @typescript-eslint/ban-types
-export default (EventClass: Function) => {
+export default (EventClass: Event) => {
 	// Make sure the provided EventClass has Event as the parent somewhere as a parent. Not Event itself, as that constructor requires an additional constructor parameter
 	// So any class that extends Event, or that extends another class that extends Event, will be allowed.
 	let classPrototype = Object.getPrototypeOf(EventClass);