Ver código fonte

feat: Implement logout functionality

Owen Diffey 3 semanas atrás
pai
commit
03d542ff5a

+ 10 - 0
backend/src/modules/DataModule/models/Session/events/SessionDeletedEvent.ts

@@ -0,0 +1,10 @@
+import Session from "@models/Session";
+import User from "@models/User";
+import ModelDeletedEvent from "@/modules/DataModule/ModelDeletedEvent";
+
+export default abstract class SessionDeletedEvent extends ModelDeletedEvent {
+	protected static _model = Session;
+
+	protected static _hasModelPermission = (model?: Session, user?: User) =>
+		model && user && model.userId === user._id;
+}

+ 9 - 1
backend/src/modules/DataModule/models/User.ts

@@ -332,7 +332,15 @@ export const setup = async () => {
 
 	// User.afterSave(async record => {});
 
-	// User.afterDestroy(async record => {});
+	User.addHook("beforeDestroy", async (record: User, options) => {
+		await Session.destroy({
+			where: {
+				userId: user._id
+			},
+			individualHooks: true,
+			transaction: options.transaction
+		});
+	});
 };
 
 export default User;

+ 13 - 0
backend/src/modules/DataModule/models/User/jobs/Logout.ts

@@ -0,0 +1,13 @@
+import User from "@models/User";
+import DataModuleJob from "@/modules/DataModule/DataModuleJob";
+import isLoggedIn from "@/modules/DataModule/permissions/isLoggedIn";
+
+export default class Logout extends DataModuleJob {
+	protected static _model = User;
+
+	protected static _hasPermission = isLoggedIn;
+
+	protected async _execute() {
+		await this._context.getSession()!.destroy();
+	}
+}

+ 21 - 0
backend/src/modules/DataModule/models/User/jobs/LogoutAll.ts

@@ -0,0 +1,21 @@
+import User from "@models/User";
+import Session from "@models/Session";
+import DataModuleJob from "@/modules/DataModule/DataModuleJob";
+import isLoggedIn from "@/modules/DataModule/permissions/isLoggedIn";
+
+export default class LogoutAll extends DataModuleJob {
+	protected static _model = User;
+
+	protected static _hasPermission = isLoggedIn;
+
+	protected async _execute() {
+		const user = await this._context.getUser();
+
+		await Session.destroy({
+			where: {
+				userId: user._id
+			},
+			individualHooks: true
+		});
+	}
+}

+ 1 - 1
backend/src/modules/DataModule/permissions/isLoggedIn.ts

@@ -1,3 +1,3 @@
 import User from "../models/User";
 
-export default (user: User) => user;
+export default (user?: User) => !!user;

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

@@ -72,6 +72,26 @@ export class WebSocketModule extends BaseModule {
 			this.dispatch(socketId, "jobCallback", callbackRef, data);
 		});
 
+		await EventsModule.pSubscribe(
+			"data.sessions.deleted:*",
+			async event => {
+				// assertEventDerived(event);
+				const { oldDoc } = event.getData();
+
+				for (const clients of this._wsServer!.clients.entries() as IterableIterator<
+					[WebSocket, WebSocket]
+				>) {
+					const socket = clients.find(
+						socket => socket.getSessionId() === oldDoc._id
+					);
+
+					if (!socket) continue;
+
+					socket.close(1000, "logout");
+				}
+			}
+		);
+
 		await super._started();
 	}
 
@@ -300,7 +320,7 @@ export class WebSocketModule extends BaseModule {
 	}
 
 	/**
-	 * getSocket - Get websocket client by id
+	 * getSocketById - Get websocket client by id
 	 */
 	public async getSocketById(socketId: string) {
 		if (!this._wsServer) return null;

+ 6 - 4
frontend/src/pages/Settings/Tabs/Security.vue

@@ -4,6 +4,7 @@ import Toast from "toasters";
 import { storeToRefs } from "pinia";
 import { useConfigStore } from "@/stores/config";
 import { useSettingsStore } from "@/stores/settings";
+import { useWebsocketStore } from "@/stores/websocket";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useUserAuthStore } from "@/stores/userAuth";
 import _validation from "@/validation";
@@ -19,6 +20,7 @@ const configStore = useConfigStore();
 const { githubAuthentication, sitename } = storeToRefs(configStore);
 const settingsStore = useSettingsStore();
 const userAuthStore = useUserAuthStore();
+const { runJob } = useWebsocketStore();
 
 const { socket } = useWebsocketsStore();
 
@@ -90,10 +92,10 @@ const unlinkGitHub = () => {
 		new Toast(res.message);
 	});
 };
-const removeSessions = () => {
-	socket.dispatch(`users.removeSessions`, currentUser.value?._id, res => {
-		new Toast(res.message);
-	});
+const removeSessions = async () => {
+	await runJob("data.users.logoutAll");
+
+	new Toast("Successfully logged out of all sessions");
 };
 
 watch(validation, newValidation => {

+ 1 - 5
frontend/src/stores/userAuth.ts

@@ -129,11 +129,7 @@ export const useUserAuthStore = defineStore("userAuth", () => {
 	};
 
 	const logout = async () => {
-		await websocketStore.runJob("users.logout", {});
-
-		document.cookie = `${configStore.cookie}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
-
-		window.location.reload();
+		await websocketStore.runJob("data.users.logout"); // TODO: Deal with socket closing before callback received
 	};
 
 	const mapUserId = (data: {

+ 9 - 1
frontend/src/stores/websocket.ts

@@ -244,9 +244,17 @@ export const useWebsocketStore = defineStore("websocket", () => {
 		);
 	};
 
-	const onClose = () => {
+	const onClose = (event: CloseEvent) => {
 		ready.value = false;
 
+		if (event.reason === "logout") {
+			document.cookie = `${configStore.cookie}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
+
+			window.location.reload();
+
+			return;
+		}
+
 		// try to reconnect every 1000ms, if the user isn't banned
 		// eslint-disable-next-line no-use-before-define
 		if (!userAuthStore.banned) setTimeout(init, 1000);