Pārlūkot izejas kodu

refactor: Update user settings form usage

Co-authored-by: Kristian Vos <k.vos@kvos.dev>
Owen Diffey 3 nedēļas atpakaļ
vecāks
revīzija
d57675cc49

+ 3 - 5
backend/src/modules/DataModule/models/User/jobs/FindById.ts

@@ -1,12 +1,10 @@
 import FindByIdJob from "@/modules/DataModule/FindByIdJob";
 import User from "../../User";
+import isAdmin from "@/modules/DataModule/permissions/isAdmin";
+import isSelf from "@/modules/DataModule/permissions/modelPermissions/isSelf";
 
 export default class FindById extends FindByIdJob {
 	protected static _model = User;
 
-	protected static _hasModelPermission = this._isSelf;
-
-	protected static _isSelf(model: User, user?: User) {
-		return model._id === user?._id;
-	}
+	protected static _hasModelPermission = [isAdmin, isSelf];
 }

+ 15 - 0
backend/src/modules/DataModule/models/User/jobs/UpdateById.ts

@@ -1,6 +1,8 @@
 import Joi from "joi";
 import UpdateByIdJob from "@/modules/DataModule/UpdateByIdJob";
 import User from "../../User";
+import { UserAvatarType } from "../UserAvatarType";
+import { UserAvatarColor } from "../UserAvatarColor";
 import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 import isSelf from "@/modules/DataModule/permissions/modelPermissions/isSelf";
 
@@ -14,7 +16,20 @@ export default class UpdateById extends UpdateByIdJob {
 			.pattern(/^[0-9a-fA-F]{24}$/)
 			.required(),
 		query: Joi.object({
+			username: Joi.string().max(64).optional(), // TODO: format and unique validation
+			emailAddress: Joi.string().max(64).optional(), // TODO: format and unique validation
+			name: Joi.string().max(64).optional(),
+			location: Joi.string().max(50).optional().allow(null, ""),
+			bio: Joi.string().max(200).optional().allow(null, ""), // TODO: Nullify empty strings
+			avatarType: Joi.valid(...Object.values(UserAvatarType)).optional(),
+			avatarColor: Joi.valid(...Object.values(UserAvatarColor))
+				.optional()
+				.allow(null),
 			nightmode: Joi.boolean().optional(),
+			autoSkipDisliked: Joi.boolean().optional(),
+			activityLogPublic: Joi.boolean().optional(),
+			anonymousSongRequests: Joi.boolean().optional(),
+			activityWatch: Joi.boolean().optional()
 		}).required()
 	});
 }

+ 78 - 177
frontend/src/pages/Settings/Tabs/Account.vue

@@ -1,13 +1,16 @@
 <script setup lang="ts">
-import { defineAsyncComponent, ref, watch, reactive, onMounted } from "vue";
+import { defineAsyncComponent, onMounted } from "vue";
 import { useRoute } from "vue-router";
 import Toast from "toasters";
 import { storeToRefs } from "pinia";
-import { useSettingsStore } from "@/stores/settings";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useUserAuthStore } from "@/stores/userAuth";
 import { useModalsStore } from "@/stores/modals";
-import _validation from "@/validation";
+import validation from "@/validation";
+import { useEvents } from "@/composables/useEvents";
+import { useModels } from "@/composables/useModels";
+import { useWebsocketStore } from "@/stores/websocket";
+import { useForm } from "@/composables/useForm";
 
 const InputHelpBox = defineAsyncComponent(
 	() => import("@/components/InputHelpBox.vue")
@@ -19,127 +22,66 @@ const QuickConfirm = defineAsyncComponent(
 	() => import("@/components/QuickConfirm.vue")
 );
 
-const settingsStore = useSettingsStore();
-const userAuthStore = useUserAuthStore();
 const route = useRoute();
 
 const { socket } = useWebsocketsStore();
 
-const saveButton = ref();
-
-const { currentUser } = storeToRefs(userAuthStore);
-const { originalUser, modifiedUser } = settingsStore;
-
-const validation = reactive({
-	username: {
-		entered: false,
-		valid: false,
-		message: "Please enter a valid username."
-	},
-	email: {
-		entered: false,
-		valid: false,
-		message: "Please enter a valid email address."
-	}
-});
-
-const { updateOriginalUser } = settingsStore;
-
 const { openModal } = useModalsStore();
 
-const onInput = inputName => {
-	validation[inputName].entered = true;
-};
-
-const changeEmail = () => {
-	const email = modifiedUser.email.address;
-	if (!_validation.isLength(email, 3, 254))
-		return new Toast("Email must have between 3 and 254 characters.");
-	if (
-		email.indexOf("@") !== email.lastIndexOf("@") ||
-		!_validation.regex.emailSimple.test(email)
-	)
-		return new Toast("Invalid email format.");
-
-	saveButton.value.saveStatus = "disabled";
+const { runJob } = useWebsocketStore();
+const { onReady } = useEvents();
+const { registerModel } = useModels();
 
-	return socket.dispatch(
-		"users.updateEmail",
-		currentUser.value?._id,
-		email,
-		res => {
-			if (res.status !== "success") {
-				new Toast(res.message);
-				saveButton.value.handleFailedSave();
-			} else {
-				new Toast("Successfully changed email address");
+const userAuthStore = useUserAuthStore();
 
-				updateOriginalUser({
-					property: "email.address",
-					value: email
-				});
+const { currentUser } = storeToRefs(userAuthStore);
 
-				saveButton.value.handleSuccessfulSave();
+const { inputs, saveButton, validate, save, setModelValues } = useForm(
+	{
+		username: {
+			value: null,
+			validate: (value: string) => {
+				if (!validation.isLength(value, 2, 32))
+					return "Username must have between 2 and 32 characters.";
+				if (!validation.regex.azAZ09_.test(value))
+					return "Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _.";
+				if (value.replaceAll(/[_]/g, "").length === 0)
+					return "Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _, and there has to be at least one letter or number.";
+				return true;
+			}
+		},
+		emailAddress: {
+			value: null,
+			validate: (value: string) => {
+				if (!validation.isLength(value, 3, 254))
+					return "Email address must have between 3 and 254 characters.";
+				if (
+					value.indexOf("@") !== value.lastIndexOf("@") ||
+					!validation.regex.emailSimple.test(value)
+				)
+					return "Invalid email address format.";
+				return true;
 			}
 		}
-	);
-};
-
-const changeUsername = () => {
-	const { username } = modifiedUser;
-
-	if (!_validation.isLength(username, 2, 32))
-		return new Toast("Username must have between 2 and 32 characters.");
-
-	if (!_validation.regex.azAZ09_.test(username))
-		return new Toast(
-			"Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _."
-		);
-
-	if (username.replaceAll(/[_]/g, "").length === 0)
-		return new Toast(
-			"Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _, and there has to be at least one letter or number."
-		);
-
-	saveButton.value.saveStatus = "disabled";
-
-	return socket.dispatch(
-		"users.updateUsername",
-		currentUser.value?._id,
-		username,
-		res => {
-			if (res.status !== "success") {
-				new Toast(res.message);
-				saveButton.value.handleFailedSave();
-			} else {
-				new Toast("Successfully changed username");
-
-				updateOriginalUser({
-					property: "username",
-					value: username
+	},
+	({ status, messages, values }, resolve, reject) => {
+		if (status === "success") {
+			runJob(`data.users.updateById`, {
+				_id: currentUser.value._id,
+				query: values
+			})
+				.then(resolve)
+				.catch(reject);
+		} else {
+			if (status === "unchanged") new Toast(messages.unchanged);
+			else if (status === "error")
+				Object.values(messages).forEach(message => {
+					new Toast({ content: message, timeout: 8000 });
 				});
-
-				saveButton.value.handleSuccessfulSave();
-			}
+			resolve();
 		}
-	);
-};
-
-const saveChanges = () => {
-	const usernameChanged = modifiedUser.username !== originalUser.username;
-	const emailAddressChanged =
-		modifiedUser.email.address !== originalUser.email.address;
-
-	if (usernameChanged) changeUsername();
-
-	if (emailAddressChanged) changeEmail();
-
-	if (!usernameChanged && !emailAddressChanged) {
-		saveButton.value.handleFailedSave();
-
-		new Toast("Please make a change before saving.");
 	}
-};
+);
 
 const removeActivities = () => {
 	socket.dispatch("activities.removeAllForUser", res => {
@@ -147,7 +89,14 @@ const removeActivities = () => {
 	});
 };
 
-onMounted(() => {
+onMounted(async () => {
+	await onReady(async () => {
+		setModelValues(await registerModel(currentUser.value), [
+			"username",
+			"emailAddress"
+		]);
+	});
+
 	if (
 		route.query.removeAccount === "relinked-github" &&
 		!localStorage.getItem("github_redirect")
@@ -158,55 +107,6 @@ onMounted(() => {
 		});
 	}
 });
-
-watch(
-	() => modifiedUser.username,
-	value => {
-		// const value = newModifiedUser.username;
-
-		if (!_validation.isLength(value, 2, 32)) {
-			validation.username.message =
-				"Username must have between 2 and 32 characters.";
-			validation.username.valid = false;
-		} else if (
-			!_validation.regex.azAZ09_.test(value) &&
-			value !== originalUser.username // Sometimes a username pulled from GitHub won't succeed validation
-		) {
-			validation.username.message =
-				"Invalid format. Allowed characters: a-z, A-Z, 0-9 and _.";
-			validation.username.valid = false;
-		} else if (value.replaceAll(/[_]/g, "").length === 0) {
-			validation.username.message =
-				"Invalid format. Allowed characters: a-z, A-Z, 0-9 and _, and there has to be at least one letter or number.";
-			validation.username.valid = false;
-		} else {
-			validation.username.message = "Everything looks great!";
-			validation.username.valid = true;
-		}
-	}
-);
-
-watch(
-	() => modifiedUser.email.address,
-	value => {
-		// const value = newModifiedUser.email.address;
-
-		if (!_validation.isLength(value, 3, 254)) {
-			validation.email.message =
-				"Email must have between 3 and 254 characters.";
-			validation.email.valid = false;
-		} else if (
-			value.indexOf("@") !== value.lastIndexOf("@") ||
-			!_validation.regex.emailSimple.test(value)
-		) {
-			validation.email.message = "Invalid format.";
-			validation.email.valid = false;
-		} else {
-			validation.email.message = "Everything looks great!";
-			validation.email.valid = true;
-		}
-	}
-);
 </script>
 
 <template>
@@ -224,21 +124,22 @@ watch(
 				id="username"
 				type="text"
 				placeholder="Enter username here..."
-				v-model="modifiedUser.username"
+				v-model="inputs.username.value"
+				@input="validate('username')"
 				maxlength="32"
 				autocomplete="off"
-				@keypress="onInput('username')"
-				@paste="onInput('username')"
 			/>
-			<span v-if="modifiedUser.username" class="character-counter"
-				>{{ modifiedUser.username.length }}/32</span
-			>
+			<span v-if="inputs.username.value" class="character-counter">
+				{{ inputs.username.value.length }}/32
+			</span>
 		</p>
 		<transition name="fadein-helpbox">
 			<input-help-box
-				:entered="validation.username.entered"
-				:valid="validation.username.valid"
-				:message="validation.username.message"
+				:entered="inputs.username.value?.length > 1"
+				:valid="inputs.username.errors.length === 0"
+				:message="
+					inputs.username.errors[0] ?? 'Everything looks great!'
+				"
 			/>
 		</transition>
 
@@ -249,22 +150,22 @@ watch(
 				id="email"
 				type="text"
 				placeholder="Enter email address here..."
-				v-if="modifiedUser.email"
-				v-model="modifiedUser.email.address"
-				@keypress="onInput('email')"
-				@paste="onInput('email')"
+				v-model="inputs.emailAddress.value"
+				@input="validate('emailAddress')"
 				autocomplete="off"
 			/>
 		</p>
 		<transition name="fadein-helpbox">
 			<input-help-box
-				:entered="validation.email.entered"
-				:valid="validation.email.valid"
-				:message="validation.email.message"
+				:entered="inputs.emailAddress.value?.length > 1"
+				:valid="inputs.emailAddress.errors.length === 0"
+				:message="
+					inputs.emailAddress.errors[0] ?? 'Everything looks great!'
+				"
 			/>
 		</transition>
 
-		<SaveButton ref="saveButton" @clicked="saveChanges()" />
+		<SaveButton ref="saveButton" @clicked="save()" />
 
 		<div class="section-margin-bottom" />
 

+ 54 - 96
frontend/src/pages/Settings/Tabs/Preferences.vue

@@ -1,103 +1,61 @@
 <script setup lang="ts">
-import { defineAsyncComponent, ref, onMounted } from "vue";
+import { defineAsyncComponent, onMounted } from "vue";
 import Toast from "toasters";
 import { storeToRefs } from "pinia";
-import { useWebsocketsStore } from "@/stores/websockets";
-import { useUserPreferencesStore } from "@/stores/userPreferences";
+import { useForm } from "@/composables/useForm";
+import { useEvents } from "@/composables/useEvents";
+import { useModels } from "@/composables/useModels";
+import { useWebsocketStore } from "@/stores/websocket";
+import { useUserAuthStore } from "@/stores/userAuth";
 
 const SaveButton = defineAsyncComponent(
 	() => import("@/components/SaveButton.vue")
 );
 
-const { socket } = useWebsocketsStore();
-const userPreferencesStore = useUserPreferencesStore();
-
-const saveButton = ref();
-
-const localNightmode = ref(false);
-const localAutoSkipDisliked = ref(false);
-const localActivityLogPublic = ref(false);
-const localAnonymousSongRequests = ref(false);
-const localActivityWatch = ref(false);
-
-const {
-	nightmode,
-	autoSkipDisliked,
-	activityLogPublic,
-	anonymousSongRequests,
-	activityWatch
-} = storeToRefs(userPreferencesStore);
-
-const saveChanges = () => {
-	if (
-		localNightmode.value === nightmode.value &&
-		localAutoSkipDisliked.value === autoSkipDisliked.value &&
-		localActivityLogPublic.value === activityLogPublic.value &&
-		localAnonymousSongRequests.value === anonymousSongRequests.value &&
-		localActivityWatch.value === activityWatch.value
-	) {
-		new Toast("Please make a change before saving.");
-
-		return saveButton.value.handleFailedSave();
-	}
-
-	saveButton.value.status = "disabled";
-
-	return socket.dispatch(
-		"users.updatePreferences",
-		{
-			nightmode: localNightmode.value,
-			autoSkipDisliked: localAutoSkipDisliked.value,
-			activityLogPublic: localActivityLogPublic.value,
-			anonymousSongRequests: localAnonymousSongRequests.value,
-			activityWatch: localActivityWatch.value
-		},
-		res => {
-			if (res.status !== "success") {
-				new Toast(res.message);
-				return saveButton.value.handleFailedSave();
-			}
-
-			new Toast("Successfully updated preferences");
-			return saveButton.value.handleSuccessfulSave();
+const { runJob } = useWebsocketStore();
+const { onReady } = useEvents();
+const { registerModel } = useModels();
+
+const userAuthStore = useUserAuthStore();
+
+const { currentUser } = storeToRefs(userAuthStore);
+
+const { inputs, saveButton, save, setModelValues } = useForm(
+	{
+		nightmode: false,
+		autoSkipDisliked: false,
+		activityLogPublic: false,
+		anonymousSongRequests: false,
+		activityWatch: false
+	},
+	({ status, messages, values }, resolve, reject) => {
+		if (status === "success") {
+			runJob(`data.users.updateById`, {
+				_id: currentUser.value._id,
+				query: values
+			})
+				.then(resolve)
+				.catch(reject);
+		} else {
+			if (status === "unchanged") new Toast(messages.unchanged);
+			else if (status === "error")
+				Object.values(messages).forEach(message => {
+					new Toast({ content: message, timeout: 8000 });
+				});
+			resolve();
 		}
-	);
-};
-
-onMounted(() => {
-	socket.onConnect(() => {
-		socket.dispatch("users.getPreferences", res => {
-			const { preferences } = res.data;
-
-			if (res.status === "success") {
-				localNightmode.value = preferences.nightmode;
-				localAutoSkipDisliked.value = preferences.autoSkipDisliked;
-				localActivityLogPublic.value = preferences.activityLogPublic;
-				localAnonymousSongRequests.value =
-					preferences.anonymousSongRequests;
-				localActivityWatch.value = preferences.activityWatch;
-			}
-		});
-	});
-
-	socket.on("keep.event:user.preferences.updated", res => {
-		const { preferences } = res.data;
-
-		if (preferences.nightmode !== undefined)
-			localNightmode.value = preferences.nightmode;
-
-		if (preferences.autoSkipDisliked !== undefined)
-			localAutoSkipDisliked.value = preferences.autoSkipDisliked;
-
-		if (preferences.activityLogPublic !== undefined)
-			localActivityLogPublic.value = preferences.activityLogPublic;
-
-		if (preferences.anonymousSongRequests !== undefined)
-			localAnonymousSongRequests.value =
-				preferences.anonymousSongRequests;
+	}
+);
 
-		if (preferences.activityWatch !== undefined)
-			localActivityWatch.value = preferences.activityWatch;
+onMounted(async () => {
+	await onReady(async () => {
+		setModelValues(await registerModel(currentUser.value), [
+			"nightmode",
+			"autoSkipDisliked",
+			"activityLogPublic",
+			"anonymousSongRequests",
+			"activityWatch"
+		]);
 	});
 });
 </script>
@@ -115,7 +73,7 @@ onMounted(() => {
 				<input
 					type="checkbox"
 					id="nightmode"
-					v-model="localNightmode"
+					v-model="inputs.nightmode.value"
 				/>
 				<span class="slider round"></span>
 			</label>
@@ -130,7 +88,7 @@ onMounted(() => {
 				<input
 					type="checkbox"
 					id="autoSkipDisliked"
-					v-model="localAutoSkipDisliked"
+					v-model="inputs.autoSkipDisliked.value"
 				/>
 				<span class="slider round"></span>
 			</label>
@@ -145,7 +103,7 @@ onMounted(() => {
 				<input
 					type="checkbox"
 					id="activityLogPublic"
-					v-model="localActivityLogPublic"
+					v-model="inputs.activityLogPublic.value"
 				/>
 				<span class="slider round"></span>
 			</label>
@@ -160,7 +118,7 @@ onMounted(() => {
 				<input
 					type="checkbox"
 					id="anonymousSongRequests"
-					v-model="localAnonymousSongRequests"
+					v-model="inputs.anonymousSongRequests.value"
 				/>
 				<span class="slider round"></span>
 			</label>
@@ -176,7 +134,7 @@ onMounted(() => {
 				<input
 					type="checkbox"
 					id="activityWatch"
-					v-model="localActivityWatch"
+					v-model="inputs.activityWatch.value"
 				/>
 				<span class="slider round"></span>
 			</label>
@@ -187,6 +145,6 @@ onMounted(() => {
 			</label>
 		</p>
 
-		<SaveButton ref="saveButton" @clicked="saveChanges()" />
+		<SaveButton ref="saveButton" @clicked="save()" />
 	</div>
 </template>

+ 93 - 175
frontend/src/pages/Settings/Tabs/Profile.vue

@@ -1,11 +1,13 @@
 <script setup lang="ts">
-import { defineAsyncComponent, ref } from "vue";
+import { defineAsyncComponent, onMounted } from "vue";
 import Toast from "toasters";
 import { storeToRefs } from "pinia";
-import { useSettingsStore } from "@/stores/settings";
-import { useWebsocketsStore } from "@/stores/websockets";
 import { useUserAuthStore } from "@/stores/userAuth";
 import validation from "@/validation";
+import { useForm } from "@/composables/useForm";
+import { useEvents } from "@/composables/useEvents";
+import { useModels } from "@/composables/useModels";
+import { useWebsocketStore } from "@/stores/websocket";
 
 const ProfilePicture = defineAsyncComponent(
 	() => import("@/components/ProfilePicture.vue")
@@ -14,164 +16,81 @@ const SaveButton = defineAsyncComponent(
 	() => import("@/components/SaveButton.vue")
 );
 
-const settingsStore = useSettingsStore();
-const userAuthStore = useUserAuthStore();
-
-const { socket } = useWebsocketsStore();
+const { runJob } = useWebsocketStore();
+const { onReady } = useEvents();
+const { registerModel } = useModels();
 
-const saveButton = ref();
+const userAuthStore = useUserAuthStore();
 
 const { currentUser } = storeToRefs(userAuthStore);
-const { originalUser, modifiedUser } = settingsStore;
-
-const { updateOriginalUser } = settingsStore;
-
-const changeName = () => {
-	modifiedUser.name = modifiedUser.name.replaceAll(/ +/g, " ").trim();
-	const { name } = modifiedUser;
-
-	if (!validation.isLength(name, 1, 64))
-		return new Toast("Name must have between 1 and 64 characters.");
-
-	if (!validation.regex.name.test(name))
-		return new Toast(
-			"Invalid name format. Only letters, numbers, spaces, apostrophes, underscores and hyphens are allowed."
-		);
-	if (name.replaceAll(/[ .'_-]/g, "").length === 0)
-		return new Toast(
-			"Invalid name format. Only letters, numbers, spaces, apostrophes, underscores and hyphens are allowed, and there has to be at least one letter or number."
-		);
-
-	saveButton.value.status = "disabled";
-
-	return socket.dispatch(
-		"users.updateName",
-		currentUser.value?._id,
-		name,
-		res => {
-			if (res.status !== "success") {
-				new Toast(res.message);
-				saveButton.value.handleFailedSave();
-			} else {
-				new Toast("Successfully changed name");
-
-				updateOriginalUser({
-					property: "name",
-					value: name
-				});
 
-				saveButton.value.handleSuccessfulSave();
+const { inputs, saveButton, save, setModelValues } = useForm(
+	{
+		name: {
+			value: null,
+			validate: (value: string) => {
+				if (!validation.isLength(value, 1, 64))
+					return "Name must have between 1 and 64 characters.";
+				if (!validation.regex.name.test(value))
+					return "Invalid name format. Only letters, numbers, spaces, apostrophes, underscores and hyphens are allowed.";
+				if (value.replaceAll(/[ .'_-]/g, "").length === 0)
+					return "Invalid name format. Only letters, numbers, spaces, apostrophes, underscores and hyphens are allowed, and there has to be at least one letter or number.";
+				return true;
 			}
-		}
-	);
-};
-
-const changeLocation = () => {
-	const { location } = modifiedUser;
-
-	if (!validation.isLength(location, 0, 50))
-		return new Toast("Location must have between 0 and 50 characters.");
-
-	saveButton.value.status = "disabled";
-
-	return socket.dispatch(
-		"users.updateLocation",
-		currentUser.value?._id,
-		location,
-		res => {
-			if (res.status !== "success") {
-				new Toast(res.message);
-				saveButton.value.handleFailedSave();
-			} else {
-				new Toast("Successfully changed location");
-
-				updateOriginalUser({
-					property: "location",
-					value: location
-				});
-
-				saveButton.value.handleSuccessfulSave();
+		},
+		location: {
+			value: null,
+			required: false,
+			validate: (value?: string) => {
+				if (value === null) return true;
+				if (!validation.isLength(value, 0, 50))
+					return "Location must be less than 50 characters.";
+				return true;
 			}
-		}
-	);
-};
-
-const changeBio = () => {
-	const { bio } = modifiedUser;
-
-	if (!validation.isLength(bio, 0, 200))
-		return new Toast("Bio must have between 0 and 200 characters.");
-
-	saveButton.value.status = "disabled";
-
-	return socket.dispatch(
-		"users.updateBio",
-		currentUser.value?._id,
-		bio,
-		res => {
-			if (res.status !== "success") {
-				new Toast(res.message);
-				saveButton.value.handleFailedSave();
-			} else {
-				new Toast("Successfully changed bio");
-
-				updateOriginalUser({
-					property: "bio",
-					value: bio
-				});
-
-				saveButton.value.handleSuccessfulSave();
+		},
+		bio: {
+			value: null,
+			required: false,
+			validate: (value?: string) => {
+				if (value === null) return true;
+				if (!validation.isLength(value, 0, 200))
+					return "Bio must be less than 200 characters.";
+				return true;
 			}
-		}
-	);
-};
-
-const changeAvatar = () => {
-	const { avatar } = modifiedUser;
-
-	saveButton.value.status = "disabled";
-
-	return socket.dispatch(
-		"users.updateAvatar",
-		currentUser.value?._id,
-		avatar,
-		res => {
-			if (res.status !== "success") {
-				new Toast(res.message);
-				saveButton.value.handleFailedSave();
-			} else {
-				new Toast("Successfully updated avatar");
-
-				updateOriginalUser({
-					property: "avatar",
-					value: avatar
+		},
+		avatarType: "initials",
+		avatarColor: "blue"
+	},
+	({ status, messages, values }, resolve, reject) => {
+		if (status === "success") {
+			runJob(`data.users.updateById`, {
+				_id: currentUser.value._id,
+				query: values
+			})
+				.then(resolve)
+				.catch(reject);
+		} else {
+			if (status === "unchanged") new Toast(messages.unchanged);
+			else if (status === "error")
+				Object.values(messages).forEach(message => {
+					new Toast({ content: message, timeout: 8000 });
 				});
-
-				saveButton.value.handleSuccessfulSave();
-			}
+			resolve();
 		}
-	);
-};
-
-const saveChanges = () => {
-	const nameChanged = modifiedUser.name !== originalUser.name;
-	const locationChanged = modifiedUser.location !== originalUser.location;
-	const bioChanged = modifiedUser.bio !== originalUser.bio;
-	const avatarChanged =
-		modifiedUser.avatar.type !== originalUser.avatar.type ||
-		modifiedUser.avatar.color !== originalUser.avatar.color;
-
-	if (nameChanged) changeName();
-	if (locationChanged) changeLocation();
-	if (bioChanged) changeBio();
-	if (avatarChanged) changeAvatar();
-
-	if (!avatarChanged && !bioChanged && !locationChanged && !nameChanged) {
-		saveButton.value.handleFailedSave();
-
-		new Toast("Please make a change before saving.");
 	}
-};
+);
+
+onMounted(async () => {
+	await onReady(async () => {
+		setModelValues(await registerModel(currentUser.value), [
+			"name",
+			"location",
+			"bio",
+			"avatarType",
+			"avatarColor"
+		]);
+	});
+});
 </script>
 
 <template>
@@ -183,31 +102,30 @@ const saveChanges = () => {
 
 		<hr class="section-horizontal-rule" />
 
-		<div
-			class="control is-expanded avatar-selection-outer-container"
-			v-if="modifiedUser.avatar"
-		>
+		<div class="control is-expanded avatar-selection-outer-container">
 			<label>Avatar</label>
 			<div id="avatar-selection-inner-container">
 				<profile-picture
-					:avatar="modifiedUser.avatar"
+					:type="inputs.avatarType.value"
+					:color="inputs.avatarColor.value"
+					:url="currentUser.avatarUrl"
 					:name="
-						modifiedUser.name
-							? modifiedUser.name
-							: modifiedUser.username
+						currentUser.name
+							? currentUser.name
+							: currentUser.username
 					"
 				/>
 				<div class="select">
-					<select v-model="modifiedUser.avatar.type">
+					<select v-model="inputs.avatarType.value">
 						<option value="gravatar">Using Gravatar</option>
 						<option value="initials">Based on initials</option>
 					</select>
 				</div>
 				<div
 					class="select"
-					v-if="modifiedUser.avatar.type === 'initials'"
+					v-if="inputs.avatarType.value === 'initials'"
 				>
-					<select v-model="modifiedUser.avatar.color">
+					<select v-model="inputs.avatarColor.value">
 						<option value="blue">Blue</option>
 						<option value="orange">Orange</option>
 						<option value="green">Green</option>
@@ -225,11 +143,11 @@ const saveChanges = () => {
 				type="text"
 				placeholder="Enter name here..."
 				maxlength="64"
-				v-model="modifiedUser.name"
+				v-model="inputs.name.value"
 			/>
-			<span v-if="modifiedUser.name" class="character-counter"
-				>{{ modifiedUser.name.length }}/64</span
-			>
+			<span v-if="inputs.name.value" class="character-counter">
+				{{ inputs.name.value.length }}/64
+			</span>
 		</p>
 		<p class="control is-expanded">
 			<label for="location">Location</label>
@@ -239,11 +157,11 @@ const saveChanges = () => {
 				type="text"
 				placeholder="Enter location here..."
 				maxlength="50"
-				v-model="modifiedUser.location"
+				v-model="inputs.location.value"
 			/>
-			<span v-if="modifiedUser.location" class="character-counter"
-				>{{ modifiedUser.location.length }}/50</span
-			>
+			<span v-if="inputs.location.value" class="character-counter">
+				{{ inputs.location.value.length }}/50
+			</span>
 		</p>
 		<p class="control is-expanded">
 			<label for="bio">Bio</label>
@@ -253,14 +171,14 @@ const saveChanges = () => {
 				placeholder="Enter bio here..."
 				maxlength="200"
 				autocomplete="off"
-				v-model="modifiedUser.bio"
+				v-model="inputs.bio.value"
 			/>
-			<span v-if="modifiedUser.bio" class="character-counter"
-				>{{ modifiedUser.bio.length }}/200</span
-			>
+			<span v-if="inputs.bio.value" class="character-counter">
+				{{ inputs.bio.value.length }}/200
+			</span>
 		</p>
 
-		<SaveButton ref="saveButton" @clicked="saveChanges()" />
+		<SaveButton ref="saveButton" @clicked="save()" />
 	</div>
 </template>
 

+ 1 - 46
frontend/src/pages/Settings/index.vue

@@ -1,9 +1,6 @@
 <script setup lang="ts">
 import { useRoute } from "vue-router";
 import { onMounted, defineAsyncComponent } from "vue";
-import Toast from "toasters";
-import { useSettingsStore } from "@/stores/settings";
-import { useWebsocketsStore } from "@/stores/websockets";
 import { useTabQueryHandler } from "@/composables/useTabQueryHandler";
 
 const MainHeader = defineAsyncComponent(
@@ -25,15 +22,10 @@ const PreferencesSettings = defineAsyncComponent(
 	() => import("./Tabs/Preferences.vue")
 );
 
-const settingsStore = useSettingsStore();
 const route = useRoute();
 const { tab, showTab } = useTabQueryHandler("");
 
-const { socket } = useWebsocketsStore();
-
-const { setUser, updateOriginalUser } = settingsStore;
-
-onMounted(() => {
+onMounted(async () => {
 	if (
 		route.query.tab === "profile" ||
 		route.query.tab === "security" ||
@@ -42,43 +34,6 @@ onMounted(() => {
 	)
 		tab.value = route.query.tab;
 	else tab.value = "profile";
-
-	// this.localNightmode = this.nightmode;
-
-	socket.onConnect(() => {
-		socket.dispatch("users.findBySession", res => {
-			if (res.status === "success") setUser(res.data.user);
-			else new Toast("You're not currently signed in.");
-		});
-	});
-
-	socket.on("event:user.password.linked", () =>
-		updateOriginalUser({
-			property: "password",
-			value: true
-		})
-	);
-
-	socket.on("event:user.password.unlinked", () =>
-		updateOriginalUser({
-			property: "password",
-			value: false
-		})
-	);
-
-	socket.on("event:user.github.linked", () =>
-		updateOriginalUser({
-			property: "github",
-			value: true
-		})
-	);
-
-	socket.on("event:user.github.unlinked", () =>
-		updateOriginalUser({
-			property: "github",
-			value: false
-		})
-	);
 });
 </script>