6 Commits 5e7297823d ... 522d4842f6

Author SHA1 Message Date
  Owen Diffey 522d4842f6 refactor: Register current user in model store 1 month ago
  Owen Diffey 2f8500c63c refactor: Update user advanced table usage 1 month ago
  Owen Diffey 5dc7774382 refactor: Allow users to subscribe to their own events 1 month ago
  Owen Diffey d6bf01329a refactor: Excludes user credentials by default 1 month ago
  Owen Diffey 915ac498a3 refactor: Add generated user hasPassword field 1 month ago
  Owen Diffey a83db81716 refactor: Add data module unique job name check 1 month ago

+ 5 - 1
backend/src/modules/DataModule.ts

@@ -317,7 +317,11 @@ export class DataModule extends BaseModule {
 				`${jobFile.path}/${jobFile.name}`
 			);
 
-			this._jobs[JobClass.getName()] = JobClass;
+			const jobName = JobClass.getName();
+			if (this._jobs[jobName]) {
+				throw new Error(`Two jobs with the same name: ${jobName}`);
+			}
+			this._jobs[jobName] = JobClass;
 		});
 	}
 

+ 8 - 0
backend/src/modules/DataModule/migrations/1725485641-create-users-table.ts

@@ -125,6 +125,14 @@ export const up = async ({
 		createdAt: DataTypes.DATE,
 		updatedAt: DataTypes.DATE
 	});
+
+	await sequelize.query(
+		"ALTER TABLE users " +
+			'ADD COLUMN "hasPassword" ' +
+			"BOOLEAN GENERATED ALWAYS AS " +
+			'("password" IS NOT NULL) ' +
+			"STORED"
+	);
 };
 
 export const down = async ({

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

@@ -274,6 +274,10 @@ export const schema = {
 	},
 	createdAt: DataTypes.DATE,
 	updatedAt: DataTypes.DATE,
+	hasPassword: {
+		type: DataTypes.BOOLEAN,
+		readonly: true
+	},
 	_name: {
 		type: DataTypes.VIRTUAL,
 		get() {
@@ -289,7 +293,19 @@ export const schema = {
 	}
 };
 
-export const options = {};
+export const options = {
+	defaultScope: {
+		attributes: {
+			exclude: [
+				"emailVerificationToken",
+				"password",
+				"passwordResetCode",
+				"passwordSetCode",
+				"githubAccessToken"
+			]
+		}
+	}
+};
 
 export const setup = async () => {
 	User.hasMany(Session, {

+ 2 - 1
backend/src/modules/DataModule/models/User/events/UserCreatedEvent.ts

@@ -1,9 +1,10 @@
 import ModelCreatedEvent from "@/modules/DataModule/ModelCreatedEvent";
 import isAdmin from "@/modules/DataModule/permissions/isAdmin";
+import isSelf from "@/modules/DataModule/permissions/modelPermissions/isSelf";
 import User from "../../User";
 
 export default abstract class UserCreatedEvent extends ModelCreatedEvent {
 	protected static _model = User;
 
-	protected static _hasPermission = isAdmin;
+	protected static _hasModelPermission = [isAdmin, isSelf];
 }

+ 2 - 1
backend/src/modules/DataModule/models/User/events/UserDeletedEvent.ts

@@ -1,9 +1,10 @@
 import ModelDeletedEvent from "@/modules/DataModule/ModelDeletedEvent";
 import isAdmin from "@/modules/DataModule/permissions/isAdmin";
+import isSelf from "@/modules/DataModule/permissions/modelPermissions/isSelf";
 import User from "../../User";
 
 export default abstract class UserDeletedEvent extends ModelDeletedEvent {
 	protected static _model = User;
 
-	protected static _hasPermission = isAdmin;
+	protected static _hasModelPermission = [isAdmin, isSelf];
 }

+ 2 - 2
backend/src/modules/DataModule/models/User/events/UserUpdatedEvent.ts

@@ -1,10 +1,10 @@
 import ModelUpdatedEvent from "@/modules/DataModule/ModelUpdatedEvent";
 import isAdmin from "@/modules/DataModule/permissions/isAdmin";
+import isSelf from "@/modules/DataModule/permissions/modelPermissions/isSelf";
 import User from "../../User";
 
 export default abstract class UserUpdatedEvent extends ModelUpdatedEvent {
 	protected static _model = User;
 
-	protected static _hasPermission = isAdmin;
-	// TODO maybe allow this for the current logged in user?
+	protected static _hasModelPermission = [isAdmin, isSelf];
 }

+ 4 - 0
backend/src/modules/DataModule/permissions/modelPermissions/isSelf.ts

@@ -0,0 +1,4 @@
+import User from "@models/User";
+
+export default (model?: User, user?: User) =>
+	model && user && model._id === user._id;

+ 6 - 23
frontend/src/App.vue

@@ -4,7 +4,6 @@ import { defineAsyncComponent, ref, watch, onMounted } from "vue";
 import Toast from "toasters";
 import { storeToRefs } from "pinia";
 import { GenericResponse } from "@musare_types/actions/GenericActions";
-import { GetPreferencesResponse } from "@musare_types/actions/UsersActions";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useConfigStore } from "@/stores/config";
 import { useUserAuthStore } from "@/stores/userAuth";
@@ -41,14 +40,10 @@ const broadcastChannel = ref({
 const disconnectedMessage = ref();
 
 const { christmas } = storeToRefs(configStore);
-const { loggedIn, banned } = storeToRefs(userAuthStore);
+const { currentUser, loggedIn, banned } = storeToRefs(userAuthStore);
 const { nightmode, activityWatch } = storeToRefs(userPreferencesStore);
 const {
 	changeNightmode,
-	changeAutoSkipDisliked,
-	changeActivityLogPublic,
-	changeAnonymousSongRequests,
-	changeActivityWatch
 } = userPreferencesStore;
 const { activeModals } = storeToRefs(modalsStore);
 const { openModal, closeCurrentModal } = modalsStore;
@@ -102,6 +97,11 @@ watch(christmas, enabled => {
 	if (enabled) enableChristmasMode();
 	else disableChristmasMode();
 });
+watch(currentUser, user => {
+	if (!user) return;
+
+	changeNightmode(user.nightmode);
+});
 
 onMounted(async () => {
 	document.getElementsByTagName("html")[0].style.cssText =
@@ -183,23 +183,6 @@ onMounted(async () => {
 			}
 		});
 
-		socket.dispatch(
-			"users.getPreferences",
-			(res: GetPreferencesResponse) => {
-				if (res.status === "success") {
-					const { preferences } = res.data;
-
-					changeAutoSkipDisliked(preferences.autoSkipDisliked);
-					changeNightmode(preferences.nightmode);
-					changeActivityLogPublic(preferences.activityLogPublic);
-					changeAnonymousSongRequests(
-						preferences.anonymousSongRequests
-					);
-					changeActivityWatch(preferences.activityWatch);
-				}
-			}
-		);
-
 		openModal("whatIsNew");
 
 		router.isReady().then(() => {

+ 21 - 19
frontend/src/pages/Admin/Users/index.vue

@@ -37,7 +37,7 @@ const columns = ref<TableColumn[]>([
 	{
 		name: "profilePicture",
 		displayName: "Image",
-		properties: ["avatar", "name", "username"],
+		properties: ["avatarType", "name", "username"],
 		sortable: false,
 		resizable: false,
 		minWidth: 71,
@@ -66,8 +66,8 @@ const columns = ref<TableColumn[]>([
 	{
 		name: "githubId",
 		displayName: "GitHub ID",
-		properties: ["services.github.id"],
-		sortProperty: "services.github.id",
+		properties: ["githubId"],
+		sortProperty: "githubId",
 		minWidth: 115,
 		defaultWidth: 115
 	},
@@ -88,15 +88,15 @@ const columns = ref<TableColumn[]>([
 	{
 		name: "emailAddress",
 		displayName: "Email Address",
-		properties: ["email.address"],
-		sortProperty: "email.address",
+		properties: ["emailAddress"],
+		sortProperty: "emailAddress",
 		defaultVisibility: "hidden"
 	},
 	{
 		name: "emailVerified",
 		displayName: "Email Verified",
-		properties: ["email.verified"],
-		sortProperty: "email.verified",
+		properties: ["emailVerified"],
+		sortProperty: "emailVerified",
 		defaultVisibility: "hidden",
 		minWidth: 140,
 		defaultWidth: 140
@@ -104,8 +104,8 @@ const columns = ref<TableColumn[]>([
 	{
 		name: "songsRequested",
 		displayName: "Songs Requested",
-		properties: ["statistics.songsRequested"],
-		sortProperty: "statistics.songsRequested",
+		properties: ["songsRequested"],
+		sortProperty: "songsRequested",
 		minWidth: 170,
 		defaultWidth: 170
 	}
@@ -243,7 +243,9 @@ onMounted(() => {
 			</template>
 			<template #column-profilePicture="slotProps">
 				<profile-picture
-					:avatar="slotProps.item.avatar"
+					:type="slotProps.item.avatarType"
+					:color="slotProps.item.avatarColor"
+					:url="slotProps.item.avatarUrl"
 					:name="
 						slotProps.item.name
 							? slotProps.item.name
@@ -268,9 +270,9 @@ onMounted(() => {
 			</template>
 			<template #column-githubId="slotProps">
 				<span
-					v-if="slotProps.item.services.github"
-					:title="slotProps.item.services.github.id"
-					>{{ slotProps.item.services.github.id }}</span
+					v-if="slotProps.item.githubId"
+					:title="slotProps.item.githubId"
+					>{{ slotProps.item.githubId }}</span
 				>
 			</template>
 			<template #column-hasPassword="slotProps">
@@ -284,18 +286,18 @@ onMounted(() => {
 				}}</span>
 			</template>
 			<template #column-emailAddress="slotProps">
-				<span :title="slotProps.item.email.address">{{
-					slotProps.item.email.address
+				<span :title="slotProps.item.emailAddress">{{
+					slotProps.item.emailAddress
 				}}</span>
 			</template>
 			<template #column-emailVerified="slotProps">
-				<span :title="slotProps.item.email.verified">{{
-					slotProps.item.email.verified
+				<span :title="slotProps.item.emailVerified">{{
+					slotProps.item.emailVerified
 				}}</span>
 			</template>
 			<template #column-songsRequested="slotProps">
-				<span :title="slotProps.item.statistics.songsRequested">{{
-					slotProps.item.statistics.songsRequested
+				<span :title="slotProps.item.songsRequested">{{
+					slotProps.item.songsRequested
 				}}</span>
 			</template>
 		</advanced-table>

+ 2 - 2
frontend/src/stores/userAuth.ts

@@ -3,7 +3,7 @@ import { computed, ref } from "vue";
 import validation from "@/validation";
 import { useWebsocketStore } from "@/stores/websocket";
 import { useConfigStore } from "@/stores/config";
-import { User } from "@/types/user";
+import Model from "@/Model";
 
 export const useUserAuthStore = defineStore("userAuth", () => {
 	const configStore = useConfigStore();
@@ -19,7 +19,7 @@ export const useUserAuthStore = defineStore("userAuth", () => {
 			((basicUser: { name: string; username: string }) => void)[]
 		>
 	>({});
-	const currentUser = ref<User | null>();
+	const currentUser = ref<Model | null>();
 	const banned = ref(false);
 	const ban = ref<{
 		reason?: string;

+ 8 - 3
frontend/src/stores/websocket.ts

@@ -5,6 +5,7 @@ import { forEachIn } from "@common/utils/forEachIn";
 import { useConfigStore } from "./config";
 import { useUserAuthStore } from "./userAuth";
 import ms from "@/ms";
+import { useModelStore } from "./model";
 
 export const useWebsocketStore = defineStore("websocket", () => {
 	const configStore = useConfigStore();
@@ -182,7 +183,13 @@ export const useWebsocketStore = defineStore("websocket", () => {
 	subscribe("ready", async data => {
 		configStore.$patch(data.config);
 
-		userAuthStore.currentUser = data.user;
+		const modelStore = useModelStore();
+
+		ready.value = true;
+
+		userAuthStore.currentUser = data.user
+			? await modelStore.registerModel(data.user)
+			: null;
 		userAuthStore.gotData = true;
 
 		if (userAuthStore.loggedIn) {
@@ -192,8 +199,6 @@ export const useWebsocketStore = defineStore("websocket", () => {
 		if (configStore.experimental.media_session) ms.initialize();
 		else ms.uninitialize();
 
-		ready.value = true;
-
 		await userAuthStore.updatePermissions();
 
 		await forEachIn(