Browse Source

Merge branch 'polishing' into owen-manage-station

Owen Diffey 3 years ago
parent
commit
b9d2db7d92
50 changed files with 984 additions and 615 deletions
  1. 128 0
      backend/logic/actions/users.js
  2. 1 1
      backend/logic/app.js
  3. 5 1
      backend/logic/ws.js
  4. 18 0
      backend/package-lock.json
  5. 1 1
      backend/package.json
  6. 129 2
      frontend/src/App.vue
  7. 1 1
      frontend/src/components/ActivityItem.vue
  8. 2 0
      frontend/src/components/Confirm.vue
  9. 5 1
      frontend/src/components/Modal.vue
  10. 2 12
      frontend/src/components/Queue.vue
  11. 2 2
      frontend/src/components/SongItem.vue
  12. 2 18
      frontend/src/components/layout/MainHeader.vue
  13. 1 4
      frontend/src/components/modals/CreateCommunityStation.vue
  14. 2 8
      frontend/src/components/modals/CreatePlaylist.vue
  15. 2 9
      frontend/src/components/modals/EditNews.vue
  16. 1 6
      frontend/src/components/modals/EditPlaylist.vue
  17. 4 14
      frontend/src/components/modals/EditSong.vue
  18. 1 4
      frontend/src/components/modals/EditUser.vue
  19. 27 29
      frontend/src/components/modals/Login.vue
  20. 1 1
      frontend/src/components/modals/ManageStation/Tabs/Blacklist.vue
  21. 2 7
      frontend/src/components/modals/ManageStation/Tabs/Playlists.vue
  22. 3 14
      frontend/src/components/modals/ManageStation/index.vue
  23. 26 13
      frontend/src/components/modals/Register.vue
  24. 310 0
      frontend/src/components/modals/RemoveAccount.vue
  25. 2 15
      frontend/src/components/modals/Report.vue
  26. 1 4
      frontend/src/components/modals/ViewPunishment.vue
  27. 3 10
      frontend/src/components/modals/ViewReport.vue
  28. 2 1
      frontend/src/main.js
  29. 2 2
      frontend/src/pages/Admin/tabs/HiddenSongs.vue
  30. 2 2
      frontend/src/pages/Admin/tabs/News.vue
  31. 4 4
      frontend/src/pages/Admin/tabs/Playlists.vue
  32. 2 2
      frontend/src/pages/Admin/tabs/Punishments.vue
  33. 3 6
      frontend/src/pages/Admin/tabs/Reports.vue
  34. 2 5
      frontend/src/pages/Admin/tabs/Stations.vue
  35. 2 2
      frontend/src/pages/Admin/tabs/UnverifiedSongs.vue
  36. 2 2
      frontend/src/pages/Admin/tabs/Users.vue
  37. 2 2
      frontend/src/pages/Admin/tabs/VerifiedSongs.vue
  38. 5 25
      frontend/src/pages/Home.vue
  39. 3 3
      frontend/src/pages/Profile/index.vue
  40. 3 8
      frontend/src/pages/Profile/tabs/Playlists.vue
  41. 1 1
      frontend/src/pages/Profile/tabs/RecentActivity.vue
  42. 84 195
      frontend/src/pages/ResetPassword.vue
  43. 19 5
      frontend/src/pages/Settings/index.vue
  44. 14 22
      frontend/src/pages/Settings/tabs/Account.vue
  45. 2 1
      frontend/src/pages/Settings/tabs/Security.vue
  46. 2 2
      frontend/src/pages/Station/Sidebar/Playlists.vue
  47. 7 16
      frontend/src/pages/Station/index.vue
  48. 112 96
      frontend/src/pages/Team.vue
  49. 25 36
      frontend/src/store/modules/modalVisibility.js
  50. 2 0
      frontend/src/store/modules/user.js

+ 128 - 0
backend/logic/actions/users.js

@@ -611,6 +611,134 @@ export default {
 		);
 	},
 
+	/**
+	 * Checks if user's password is correct (e.g. before a sensitive action)
+	 *
+	 * @param {object} session - the session object automatically added by the websocket
+	 * @param {string} password - the password the user entered that we need to validate
+	 * @param {Function} cb - gets called with the result
+	 */
+	confirmPasswordMatch: isLoginRequired(async function confirmPasswordMatch(session, password, cb) {
+		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
+
+		return async.waterfall(
+			[
+				next => {
+					if (!password || password === "") return next("Please provide a valid password.");
+					return next();
+				},
+
+				next => {
+					userModel.findOne({ _id: session.userId }, (err, user) =>
+						next(err, user.services.password.password)
+					);
+				},
+
+				(passwordHash, next) => {
+					if (!passwordHash) return next("Your account doesn't have a password linked.");
+
+					return bcrypt.compare(sha256(password), passwordHash, (err, match) => {
+						if (err) return next(err);
+						if (!match) return next(null, false);
+						return next(null, true);
+					});
+				}
+			],
+			async (err, match) => {
+				if (err) {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+					this.log(
+						"ERROR",
+						"USER_CONFIRM_PASSWORD",
+						`Couldn't confirm password for user "${session.userId}". "${err}"`
+					);
+					return cb({ status: "error", message: err });
+				}
+
+				if (match) {
+					this.log(
+						"SUCCESS",
+						"USER_CONFIRM_PASSWORD",
+						`Successfully checked for password match (it matched) for user "${session.userId}".`
+					);
+
+					return cb({
+						status: "success",
+						message: "Your password matches."
+					});
+				}
+
+				this.log(
+					"SUCCESS",
+					"USER_CONFIRM_PASSWORD",
+					`Successfully checked for password match (it didn't match) for user "${session.userId}".`
+				);
+
+				return cb({
+					status: "error",
+					message: "Unfortunately your password doesn't match."
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Checks if user's github access token has expired or not (ie. if their github account is still linked)
+	 *
+	 * @param {object} session - the session object automatically added by the websocket
+	 * @param {Function} cb - gets called with the result
+	 */
+	confirmGithubLink: isLoginRequired(async function confirmGithubLink(session, cb) {
+		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
+
+		return async.waterfall(
+			[
+				next => {
+					userModel.findOne({ _id: session.userId }, (err, user) => next(err, user));
+				},
+
+				(user, next) => {
+					if (!user.services.github) return next("You don't have GitHub linked to your account.");
+
+					return axios
+						.get(`https://api.github.com/user/emails`, {
+							headers: {
+								"User-Agent": "request",
+								Authorization: `token ${user.services.github.access_token}`
+							}
+						})
+						.then(res => next(null, res))
+						.catch(err => next(err));
+				},
+
+				(res, next) => next(null, res.status === 200)
+			],
+			async (err, linked) => {
+				if (err) {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+					this.log(
+						"ERROR",
+						"USER_CONFIRM_GITHUB_LINK",
+						`Couldn't confirm github link for user "${session.userId}". "${err}"`
+					);
+					return cb({ status: "error", message: err });
+				}
+
+				this.log(
+					"SUCCESS",
+					"USER_CONFIRM_GITHUB_LINK",
+					`GitHub is ${linked ? "linked" : "not linked"} for user "${session.userId}".`
+				);
+
+				return cb({
+					status: "success",
+					data: { linked },
+					message: "Successfully checked if GitHub accounty was linked."
+				});
+			}
+		);
+	}),
+
 	/**
 	 * Removes all sessions for a user
 	 *

+ 1 - 1
backend/logic/app.js

@@ -216,7 +216,7 @@ class _AppModule extends CoreClass {
 												value: user._id
 											});
 
-											res.redirect(`${config.get("domain")}/settings#security`);
+											res.redirect(`${config.get("domain")}/settings?tab=security`);
 										}
 									],
 									next

+ 5 - 1
backend/logic/ws.js

@@ -549,14 +549,18 @@ class _WSModule extends CoreClass {
 								let role = "";
 								let username = "";
 								let userId = "";
+								let email = "";
 
 								if (user) {
 									role = user.role;
 									username = user.username;
+									email = user.email.address;
 									userId = session.userId;
 								}
 
-								return socket.dispatch("ready", { data: { loggedIn: true, role, username, userId } });
+								return socket.dispatch("ready", {
+									data: { loggedIn: true, role, username, userId, email }
+								});
 							});
 						} else socket.dispatch("ready", { data: { loggedIn: false } });
 					})

+ 18 - 0
backend/package-lock.json

@@ -23,6 +23,7 @@
         "nodemailer": "^6.4.18",
         "oauth": "^0.9.15",
         "redis": "^2.8.0",
+        "retry-axios": "^2.4.0",
         "sha256": "^0.2.0",
         "underscore": "^1.10.2",
         "ws": "^7.4.3"
@@ -2867,6 +2868,17 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/retry-axios": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-2.4.0.tgz",
+      "integrity": "sha512-rK7UBYgbrNoVothbSmM0tEm9DIiXapmVUrnUYn+d9AuQvF0AY5RkJU2FQvlufe9hlFwrCdDhrJTwiyRtR7wUaA==",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "axios": "*"
+      }
+    },
     "node_modules/rimraf": {
       "version": "2.7.1",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
@@ -5786,6 +5798,12 @@
       "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
       "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
     },
+    "retry-axios": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-2.4.0.tgz",
+      "integrity": "sha512-rK7UBYgbrNoVothbSmM0tEm9DIiXapmVUrnUYn+d9AuQvF0AY5RkJU2FQvlufe9hlFwrCdDhrJTwiyRtR7wUaA==",
+      "requires": {}
+    },
     "rimraf": {
       "version": "2.7.1",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",

+ 1 - 1
backend/package.json

@@ -17,7 +17,6 @@
   "dependencies": {
     "async": "3.1.0",
     "axios": "^0.21.1",
-    "retry-axios": "^2.4.0",
     "bcrypt": "^5.0.0",
     "bluebird": "^3.5.5",
     "body-parser": "^1.19.0",
@@ -30,6 +29,7 @@
     "nodemailer": "^6.4.18",
     "oauth": "^0.9.15",
     "redis": "^2.8.0",
+    "retry-axios": "^2.4.0",
     "sha256": "^0.2.0",
     "underscore": "^1.10.2",
     "ws": "^7.4.3"

+ 129 - 2
frontend/src/App.vue

@@ -4,8 +4,8 @@
 		<div v-else class="upper-container">
 			<router-view :key="$route.fullPath" class="main-container" />
 			<what-is-new />
-			<login-modal v-if="modals.header.login" />
-			<register-modal v-if="modals.header.register" />
+			<login-modal v-if="modals.login" />
+			<register-modal v-if="modals.register" />
 		</div>
 	</div>
 </template>
@@ -246,6 +246,15 @@ export default {
 		background-color: var(--dark-grey-3) !important;
 	}
 
+	.content-box,
+	.step:not(.selected) {
+		background-color: var(--dark-grey-3) !important;
+	}
+
+	.label {
+		color: var(--light-grey-2);
+	}
+
 	.tippy-tooltip.songActions-theme {
 		background-color: var(--dark-grey);
 	}
@@ -946,4 +955,122 @@ h4.section-title {
 	mask: url("/assets/social/youtube.svg") no-repeat center;
 	background-color: var(--youtube);
 }
+
+#forgot-password {
+	display: flex;
+	justify-content: flex-start;
+	margin: 5px 0;
+}
+
+.steps-fade-enter-active,
+.steps-fade-leave-active {
+	transition: all 0.3s ease;
+}
+
+.steps-fade-enter,
+.steps-fade-leave-to {
+	opacity: 0;
+}
+
+.skip-step {
+	background-color: var(--grey-3);
+	color: var(--white);
+}
+
+#steps {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	height: 50px;
+	margin-top: 36px;
+
+	@media screen and (max-width: 300px) {
+		display: none;
+	}
+
+	.step {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		border-radius: 100%;
+		border: 1px solid var(--dark-grey);
+		min-width: 50px;
+		min-height: 50px;
+		background-color: var(--white);
+		font-size: 30px;
+		cursor: pointer;
+
+		&.selected {
+			background-color: var(--primary-color);
+			color: var(--white) !important;
+			border: 0;
+		}
+	}
+
+	.divider {
+		display: flex;
+		justify-content: center;
+		width: 180px;
+		height: 1px;
+		background-color: var(--dark-grey);
+	}
+}
+
+.content-box {
+	margin-top: 90px;
+	border-radius: 3px;
+	background-color: var(--white);
+	border: 1px solid var(--dark-grey);
+	max-width: 580px;
+	padding: 40px;
+
+	@media screen and (max-width: 300px) {
+		margin-top: 30px;
+		padding: 30px 20px;
+	}
+}
+
+.content-box-optional-helper {
+	margin-top: 15px;
+	color: var(--primary-color);
+	text-decoration: underline;
+	font-size: 16px;
+
+	a {
+		color: var(--primary-color);
+	}
+}
+
+.content-box-title {
+	font-size: 25px;
+	color: var(--black);
+}
+
+.content-box-description {
+	font-size: 14px;
+	color: var(--dark-grey);
+}
+
+.content-box-inputs {
+	margin-top: 35px;
+
+	.input-with-button {
+		.button {
+			width: 105px;
+		}
+
+		@media screen and (max-width: 450px) {
+			flex-direction: column;
+		}
+	}
+
+	label {
+		font-size: 11px;
+	}
+
+	#change-password-button {
+		margin-top: 36px;
+		width: 175px;
+	}
+}
 </style>

+ 1 - 1
frontend/src/components/ActivityItem.vue

@@ -156,7 +156,7 @@ export default {
 		},
 		showPlaylist(playlistId) {
 			this.editPlaylist(playlistId);
-			this.openModal({ sector: "station", modal: "editPlaylist" });
+			this.openModal("editPlaylist");
 		},
 		...mapActions("user/playlists", ["editPlaylist"]),
 		formatDistance,

+ 2 - 0
frontend/src/components/Confirm.vue

@@ -5,6 +5,8 @@
 		theme="confirm"
 		ref="confirm"
 		trigger="click"
+		class="button-with-tooltip"
+		@hide="clickedOnce = false"
 	>
 		<template #trigger>
 			<div @click.shift.stop="confirm(true)" @click.exact="confirm()">

+ 5 - 1
frontend/src/components/Modal.vue

@@ -97,7 +97,11 @@ p {
 	}
 	.right {
 		margin-left: auto;
-		justify-content: right;
+		justify-content: flex-end;
+
+		*:not(:last-child) {
+			margin-right: 5px;
+		}
 	}
 }
 </style>

+ 2 - 12
frontend/src/components/Queue.vue

@@ -79,12 +79,7 @@
 						!station.locked ||
 						(station.locked && isAdminOnly() && dismissedWarning))
 			"
-			@click="
-				openModal({
-					sector: 'station',
-					modal: 'manageStation'
-				})
-			"
+			@click="openModal('manageStation')"
 		>
 			<i class="material-icons icon-with-button">queue</i>
 			<span class="optional-desktop-only-text"> Add Song To Queue </span>
@@ -94,12 +89,7 @@
 			v-if="
 				sector === 'station' && loggedIn && station.type === 'official'
 			"
-			@click="
-				openModal({
-					sector: 'station',
-					modal: 'requestSong'
-				})
-			"
+			@click="openModal('requestSong')"
 		>
 			<i class="material-icons icon-with-button">queue</i>
 			<span class="optional-desktop-only-text"> Request Song </span>

+ 2 - 2
frontend/src/components/SongItem.vue

@@ -206,12 +206,12 @@ export default {
 		report(song) {
 			this.hideTippyElements();
 			this.reportSong(song);
-			this.openModal({ sector: "station", modal: "report" });
+			this.openModal("report");
 		},
 		edit(song) {
 			this.hideTippyElements();
 			this.editSong(song);
-			this.openModal({ sector: "admin", modal: "editSong" });
+			this.openModal("editSong");
 		},
 		...mapActions("modals/editSong", ["editSong"]),
 		...mapActions("modals/report", ["reportSong"]),

+ 2 - 18
frontend/src/components/layout/MainHeader.vue

@@ -45,26 +45,10 @@
 				<a class="nav-item is-tab" href="#" @click="logout()">Logout</a>
 			</span>
 			<span v-if="!loggedIn && !hideLoggedOut" class="grouped">
-				<a
-					class="nav-item"
-					href="#"
-					@click="
-						openModal({
-							sector: 'header',
-							modal: 'login'
-						})
-					"
+				<a class="nav-item" href="#" @click="openModal('login')"
 					>Login</a
 				>
-				<a
-					class="nav-item"
-					href="#"
-					@click="
-						openModal({
-							sector: 'header',
-							modal: 'register'
-						})
-					"
+				<a class="nav-item" href="#" @click="openModal('register')"
 					>Register</a
 				>
 			</span>

+ 1 - 4
frontend/src/components/modals/CreateCommunityStation.vue

@@ -109,10 +109,7 @@ export default {
 				res => {
 					if (res.status === "success") {
 						new Toast(`You have added the station successfully`);
-						this.closeModal({
-							sector: "home",
-							modal: "createCommunityStation"
-						});
+						this.closeModal("createCommunityStation");
 					} else new Toast(res.message);
 				}
 			);

+ 2 - 8
frontend/src/components/modals/CreatePlaylist.vue

@@ -73,15 +73,9 @@ export default {
 					new Toast(res.message);
 
 					if (res.status === "success") {
-						this.closeModal({
-							sector: "station",
-							modal: "createPlaylist"
-						});
+						this.closeModal("createPlaylist");
 						this.editPlaylist(res.data.playlistId);
-						this.openModal({
-							sector: "station",
-							modal: "editPlaylist"
-						});
+						this.openModal("editPlaylist");
 					}
 				}
 			);

+ 2 - 9
frontend/src/components/modals/EditNews.vue

@@ -180,10 +180,7 @@ export default {
 				this.editNews(news);
 			} else {
 				new Toast("News with that ID not found");
-				this.closeModal({
-					sector: this.sector,
-					modal: "editNews"
-				});
+				this.closeModal("editNews");
 			}
 		});
 	},
@@ -211,11 +208,7 @@ export default {
 				res => {
 					new Toast(res.message);
 					if (res.status === "success") {
-						if (close)
-							this.closeModal({
-								sector: this.sector,
-								modal: "editNews"
-							});
+						if (close) this.closeModal("editNews");
 					}
 				}
 			);

+ 1 - 6
frontend/src/components/modals/EditPlaylist.vue

@@ -646,12 +646,7 @@ export default {
 		removePlaylist() {
 			this.socket.dispatch("playlists.remove", this.playlist._id, res => {
 				new Toast(res.message);
-				if (res.status === "success") {
-					this.closeModal({
-						sector: "station",
-						modal: "editPlaylist"
-					});
-				}
+				if (res.status === "success") this.closeModal("editPlaylist");
 			});
 		},
 		async downloadPlaylist() {

+ 4 - 14
frontend/src/components/modals/EditSong.vue

@@ -585,7 +585,7 @@ export default {
 			song: state => state.song
 		}),
 		...mapState("modalVisibility", {
-			modals: state => state.modals.admin
+			modals: state => state.modals
 		}),
 		...mapGetters({
 			socket: "websockets/getSocket"
@@ -735,10 +735,7 @@ export default {
 				});
 			} else {
 				new Toast("Song with that ID not found");
-				this.closeModal({
-					sector: this.sector,
-					modal: "editSong"
-				});
+				this.closeModal("editSong");
 			}
 		});
 
@@ -838,10 +835,7 @@ export default {
 			ctrl: true,
 			preventDefault: true,
 			handler: () => {
-				this.closeModal({
-					sector: this.sector,
-					modal: "editSong"
-				});
+				this.closeModal("editSong");
 				setTimeout(() => {
 					window.focusedElementBefore.focus();
 				}, 500);
@@ -1054,11 +1048,7 @@ export default {
 					saveButtonRef.handleSuccessfulSave();
 				else saveButtonRef.handleFailedSave();
 
-				if (close)
-					this.closeModal({
-						sector: this.sector,
-						modal: "editSong"
-					});
+				if (close) this.closeModal("editSong");
 			});
 		},
 		toggleAPIResult(index) {

+ 1 - 4
frontend/src/components/modals/EditUser.vue

@@ -111,10 +111,7 @@ export default {
 				this.editUser(user);
 			} else {
 				new Toast("User with that ID not found");
-				this.closeModal({
-					sector: this.sector,
-					modal: "editUser"
-				});
+				this.closeModal("editUser");
 			}
 		});
 	},

+ 27 - 29
frontend/src/components/modals/Login.vue

@@ -53,14 +53,16 @@
 							</a>
 						</div>
 
-						<router-link
-							id="forgot-password"
-							href="#"
-							to="/reset_password"
-							@click.native="closeLoginModal()"
-						>
-							Forgot password?
-						</router-link>
+						<p class="content-box-optional-helper">
+							<router-link
+								id="forgot-password"
+								href="#"
+								to="/reset_password"
+								@click.native="closeLoginModal()"
+							>
+								Forgot password?
+							</router-link>
+						</p>
 
 						<br />
 						<p>
@@ -104,12 +106,15 @@
 							&nbsp;&nbsp;Login with GitHub
 						</a>
 					</div>
-					<router-link to="/register" v-if="isPage">
-						Don't have an account?
-					</router-link>
-					<a v-else href="#" @click="changeToRegisterModal()">
-						Don't have an account?
-					</a>
+
+					<p class="content-box-optional-helper">
+						<router-link to="/register" v-if="isPage">
+							Don't have an account?
+						</router-link>
+						<a v-else href="#" @click="changeToRegisterModal()">
+							Don't have an account?
+						</a>
+					</p>
 				</footer>
 			</div>
 		</div>
@@ -161,15 +166,15 @@ export default {
 		changeToRegisterModal() {
 			if (!this.isPage) {
 				this.closeLoginModal();
-				this.openModal({ sector: "header", modal: "register" });
+				this.openModal("register");
 			}
 		},
 		closeLoginModal() {
-			if (!this.isPage)
-				this.closeModal({ sector: "header", modal: "login" });
+			if (!this.isPage) this.closeModal("login");
 		},
 		githubRedirect() {
-			localStorage.setItem("github_redirect", this.$route.path);
+			if (!this.isPage)
+				localStorage.setItem("github_redirect", this.$route.path);
 		},
 		...mapActions("modalVisibility", ["closeModal", "openModal"]),
 		...mapActions("user/auth", ["login"])
@@ -206,17 +211,14 @@ export default {
 	}
 }
 
-#forgot-password {
-	display: flex;
-	justify-content: flex-start;
-	height: 0;
-	margin: 5px 0;
-}
-
 .modal-card-foot {
 	display: flex;
 	justify-content: space-between;
 	flex-wrap: wrap;
+
+	.content-box-optional-helper {
+		margin-top: 0;
+	}
 }
 
 .button.is-github {
@@ -234,8 +236,4 @@ export default {
 .invert {
 	filter: brightness(5);
 }
-
-a {
-	color: var(--primary-color);
-}
 </style>

+ 1 - 1
frontend/src/components/modals/ManageStation/Tabs/Blacklist.vue

@@ -100,7 +100,7 @@ export default {
 		},
 		showPlaylist(playlistId) {
 			this.editPlaylist(playlistId);
-			this.openModal({ sector: "station", modal: "editPlaylist" });
+			this.openModal("editPlaylist");
 		},
 		deselectPlaylist(id) {
 			this.socket.dispatch(

+ 2 - 7
frontend/src/components/modals/ManageStation/Tabs/Playlists.vue

@@ -189,12 +189,7 @@
 				<button
 					class="button is-primary"
 					id="create-new-playlist-button"
-					@click="
-						openModal({
-							sector: 'station',
-							modal: 'createPlaylist'
-						})
-					"
+					@click="openModal('createPlaylist')"
 				>
 					Create new playlist
 				</button>
@@ -451,7 +446,7 @@ export default {
 		},
 		showPlaylist(playlistId) {
 			this.editPlaylist(playlistId);
-			this.openModal({ sector: "station", modal: "editPlaylist" });
+			this.openModal("editPlaylist");
 		},
 		selectPlaylist(playlist) {
 			if (this.station.type === "community" && this.station.partyMode) {

+ 3 - 14
frontend/src/components/modals/ManageStation/index.vue

@@ -157,12 +157,7 @@
 			<button
 				class="button is-primary tab-actionable-button"
 				v-if="loggedIn && station.type === 'official'"
-				@click="
-					openModal({
-						sector: 'station',
-						modal: 'requestSong'
-					})
-				"
+				@click="openModal('requestSong')"
 			>
 				<i class="material-icons icon-with-button">queue</i>
 				<span class="optional-desktop-only-text"> Request Song </span>
@@ -284,10 +279,7 @@ export default {
 				);
 			} else {
 				new Toast(`Station with that ID not found`);
-				this.closeModal({
-					sector: this.sector,
-					modal: "manageStation"
-				});
+				this.closeModal("manageStation");
 			}
 		});
 
@@ -382,10 +374,7 @@ export default {
 				res => {
 					if (res.status === "success") {
 						this.editPlaylist(res.data.playlist._id);
-						this.openModal({
-							sector: "station",
-							modal: "editPlaylist"
-						});
+						this.openModal("editPlaylist");
 					} else {
 						new Toast(res.message);
 					}

+ 26 - 13
frontend/src/components/modals/Register.vue

@@ -22,6 +22,7 @@
 							type="email"
 							placeholder="Email..."
 							@keypress="onInput('email')"
+							@paste="onInput('email')"
 							autofocus
 						/>
 					</p>
@@ -30,7 +31,7 @@
 							:entered="email.entered"
 							:valid="email.valid"
 							:message="email.message"
-						></input-help-box>
+						/>
 					</transition>
 
 					<!-- username -->
@@ -42,6 +43,7 @@
 							type="text"
 							placeholder="Username..."
 							@keypress="onInput('username')"
+							@paste="onInput('username')"
 						/>
 					</p>
 					<transition name="fadein-helpbox">
@@ -49,7 +51,7 @@
 							:entered="username.entered"
 							:valid="username.valid"
 							:message="username.message"
-						></input-help-box>
+						/>
 					</transition>
 
 					<!-- password -->
@@ -68,6 +70,10 @@
 								onInput('password') &&
 									$parent.submitOnEnter(submitModal, $event)
 							"
+							@paste="
+								onInput('password') &&
+									$parent.submitOnEnter(submitModal, $event)
+							"
 						/>
 						<a @click="togglePasswordVisibility()">
 							<i class="material-icons">
@@ -85,7 +91,7 @@
 							:valid="password.valid"
 							:entered="password.entered"
 							:message="password.message"
-						></input-help-box>
+						/>
 					</transition>
 
 					<br />
@@ -129,12 +135,15 @@
 							&nbsp;&nbsp;Register with GitHub
 						</a>
 					</div>
-					<router-link to="/login" v-if="isPage">
-						Already have an account?
-					</router-link>
-					<a v-else href="#" @click="changeToLoginModal()">
-						Already have an account?
-					</a>
+
+					<p class="content-box-optional-helper">
+						<router-link to="/login" v-if="isPage">
+							Already have an account?
+						</router-link>
+						<a v-else href="#" @click="changeToLoginModal()">
+							Already have an account?
+						</a>
+					</p>
 				</footer>
 			</div>
 		</div>
@@ -272,12 +281,11 @@ export default {
 		changeToLoginModal() {
 			if (!this.isPage) {
 				this.closeRegisterModal();
-				this.openModal({ sector: "header", modal: "login" });
+				this.openModal("login");
 			}
 		},
 		closeRegisterModal() {
-			if (!this.isPage)
-				this.closeModal({ sector: "header", modal: "login" });
+			if (!this.isPage) this.closeModal("login");
 		},
 		submitModal() {
 			if (
@@ -302,7 +310,8 @@ export default {
 			this[inputName].entered = true;
 		},
 		githubRedirect() {
-			localStorage.setItem("github_redirect", this.$route.path);
+			if (!this.isPage)
+				localStorage.setItem("github_redirect", this.$route.path);
 		},
 		...mapActions("modalVisibility", ["closeModal", "openModal"]),
 		...mapActions("user/auth", ["register"])
@@ -347,6 +356,10 @@ export default {
 	display: flex;
 	justify-content: space-between;
 	flex-wrap: wrap;
+
+	.content-box-optional-helper {
+		margin-top: 0;
+	}
 }
 
 .button.is-github {

+ 310 - 0
frontend/src/components/modals/RemoveAccount.vue

@@ -0,0 +1,310 @@
+<template>
+	<modal
+		title="Confirm Account Removal"
+		class="confirm-account-removal-modal"
+	>
+		<template #body>
+			<div id="steps">
+				<p
+					class="step"
+					:class="{ selected: step === 'confirm-identity' }"
+				>
+					1
+				</p>
+				<span class="divider"></span>
+				<p
+					class="step"
+					:class="{
+						selected:
+							(isPasswordLinked && step === 'export-data') ||
+							step === 'relink-github'
+					}"
+				>
+					2
+				</p>
+				<span class="divider"></span>
+				<p
+					class="step"
+					:class="{
+						selected:
+							(isPasswordLinked && step === 'remove-account') ||
+							step === 'export-data'
+					}"
+				>
+					3
+				</p>
+				<span class="divider" v-if="!isPasswordLinked"></span>
+				<p
+					class="step"
+					:class="{ selected: step === 'remove-account' }"
+					v-if="!isPasswordLinked"
+				>
+					4
+				</p>
+			</div>
+
+			<div
+				class="content-box"
+				id="password-linked"
+				v-if="step === 'confirm-identity' && isPasswordLinked"
+			>
+				<h2 class="content-box-title">
+					Enter your password
+				</h2>
+				<p class="content-box-description">
+					We will send a code to your email address to verify your
+					identity.
+				</p>
+
+				<p class="content-box-optional-helper">
+					<router-link
+						id="forgot-password"
+						href="#"
+						to="/reset_password"
+					>
+						Forgot password?
+					</router-link>
+				</p>
+
+				<div class="content-box-inputs">
+					<div class="control is-grouped input-with-button">
+						<div id="password-container">
+							<input
+								class="input"
+								type="password"
+								placeholder="Enter password here..."
+								autofocus
+								ref="password"
+								v-model="password.value"
+							/>
+							<a @click="togglePasswordVisibility()">
+								<i class="material-icons">
+									{{
+										!password.visible
+											? "visibility"
+											: "visibility_off"
+									}}
+								</i>
+							</a>
+						</div>
+						<p class="control">
+							<a
+								class="button is-info"
+								href="#"
+								@click="confirmPasswordMatch()"
+								>Check</a
+							>
+						</p>
+					</div>
+				</div>
+			</div>
+
+			<div
+				class="content-box"
+				v-else-if="isGithubLinked && step === 'confirm-identity'"
+			>
+				<h2 class="content-box-title">Verify your GitHub</h2>
+				<p class="content-box-description">
+					Check your GitHub account is still linked in order to remove
+					your account.
+				</p>
+
+				<div class="content-box-inputs">
+					<a class="button is-github" @click="confirmGithubLink()">
+						<div class="icon">
+							<img
+								class="invert"
+								src="/assets/social/github.svg"
+							/>
+						</div>
+						&nbsp; Check GitHub is linked
+					</a>
+				</div>
+			</div>
+
+			<div class="content-box" v-if="step === 'relink-github'">
+				<h2 class="content-box-title">Re-link GitHub</h2>
+				<p class="content-box-description">
+					Re-link your GitHub account in order to verify your
+					identity.
+				</p>
+
+				<div class="content-box-inputs">
+					<a
+						class="button is-github"
+						:href="`${apiDomain}/auth/github/link`"
+					>
+						<div class="icon">
+							<img
+								class="invert"
+								src="/assets/social/github.svg"
+							/>
+						</div>
+						&nbsp; Re-link GitHub to account
+					</a>
+				</div>
+			</div>
+
+			<div v-if="step === 'export-data'">
+				DOWNLOAD A BACKUP OF YOUR DATa BEFORE ITS PERMENATNELY DELETED
+			</div>
+
+			<div
+				class="content-box"
+				id="remove-account-container"
+				v-if="step === 'remove-account'"
+			>
+				<h2 class="content-box-title">Remove your account</h2>
+				<p class="content-box-description">
+					There is no going back after confirming account removal.
+				</p>
+
+				<div class="content-box-inputs">
+					<confirm placement="right" @confirm="remove()">
+						<button class="button">
+							<i class="material-icons">delete</i>
+							&nbsp;Remove Account
+						</button>
+					</confirm>
+				</div>
+			</div>
+		</template>
+	</modal>
+</template>
+
+<script>
+import { mapActions, mapGetters } from "vuex";
+
+import Toast from "toasters";
+
+import Confirm from "@/components/Confirm.vue";
+import Modal from "../Modal.vue";
+
+export default {
+	components: { Modal, Confirm },
+	data() {
+		return {
+			step: "confirm-identity",
+			apiDomain: "",
+			password: {
+				value: "",
+				visible: false
+			}
+		};
+	},
+	computed: mapGetters({
+		isPasswordLinked: "settings/isPasswordLinked",
+		isGithubLinked: "settings/isGithubLinked",
+		socket: "websockets/getSocket"
+	}),
+	async mounted() {
+		this.apiDomain = await lofig.get("apiDomain");
+	},
+	methods: {
+		togglePasswordVisibility() {
+			if (this.$refs.password.type === "password") {
+				this.$refs.password.type = "text";
+				this.password.visible = true;
+			} else {
+				this.$refs.password.type = "password";
+				this.password.visible = false;
+			}
+		},
+		confirmPasswordMatch() {
+			return this.socket.dispatch(
+				"users.confirmPasswordMatch",
+				this.password.value,
+				res => {
+					if (res.status === "success") this.step = "remove-account";
+					else new Toast(res.message);
+				}
+			);
+		},
+		confirmGithubLink() {
+			return this.socket.dispatch("users.confirmGithubLink", res => {
+				if (res.status === "success") {
+					if (res.data.linked) this.step = "remove-account";
+					else {
+						new Toast(
+							`Your GitHub account isn't linked. Please re-link your account and try again.`
+						);
+						this.step = "relink-github";
+						localStorage.setItem(
+							"github_redirect",
+							window.location.pathname + window.location.search
+						);
+					}
+				} else new Toast(res.message);
+			});
+		},
+		remove() {
+			return this.socket.dispatch("users.remove", res => {
+				if (res.status === "success") {
+					return this.socket.dispatch("users.logout", () => {
+						return lofig.get("cookie").then(cookie => {
+							document.cookie = `${cookie.SIDname}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
+							this.closeModal("removeAccount");
+							return window.location.reload();
+						});
+					});
+				}
+
+				return new Toast(res.message);
+			});
+		},
+		...mapActions("modalVisibility", ["closeModal", "openModal"])
+	}
+};
+</script>
+
+<style lang="scss">
+.confirm-account-removal-modal {
+	.modal-card {
+		width: 650px;
+	}
+}
+</style>
+
+<style lang="scss" scoped>
+h2 {
+	margin: 0;
+}
+
+.content-box {
+	margin-top: 20px;
+	max-width: unset;
+}
+
+#steps {
+	margin-top: 0;
+}
+
+#password-linked {
+	#password-container {
+		display: flex;
+		align-items: center;
+		width: 100%; // new
+
+		a {
+			width: 0;
+			margin-left: -30px;
+			z-index: 0;
+			top: 2px;
+			position: relative;
+			color: var(--light-grey-1);
+		}
+	}
+
+	> a {
+		color: var(--primary-color);
+	}
+}
+
+.control {
+	margin-bottom: 0 !important;
+}
+
+#remove-account-container .content-box-inputs {
+	width: fit-content;
+}
+</style>

+ 2 - 15
frontend/src/components/modals/Report.vue

@@ -123,16 +123,7 @@
 				<i class="material-icons save-changes">done</i>
 				<span>&nbsp;Create</span>
 			</a>
-			<a
-				class="button is-danger"
-				href="#"
-				@click="
-					closeModal({
-						sector: 'station',
-						modal: 'report'
-					})
-				"
-			>
+			<a class="button is-danger" href="#" @click="closeModal('report')">
 				<span>&nbsp;Cancel</span>
 			</a>
 		</div>
@@ -222,11 +213,7 @@ export default {
 		create() {
 			this.socket.dispatch("reports.create", this.report, res => {
 				new Toast(res.message);
-				if (res.status === "success")
-					this.closeModal({
-						sector: "station",
-						modal: "report"
-					});
+				if (res.status === "success") this.closeModal("report");
 			});
 		},
 		highlight(type) {

+ 1 - 4
frontend/src/components/modals/ViewPunishment.vue

@@ -97,10 +97,7 @@ export default {
 					this.viewPunishment(punishment);
 				} else {
 					new Toast("Punishment with that ID not found");
-					this.closeModal({
-						sector: this.sector,
-						modal: "viewPunishment"
-					});
+					this.closeModal("viewPunishment");
 				}
 			}
 		);

+ 3 - 10
frontend/src/components/modals/ViewReport.vue

@@ -100,7 +100,7 @@ export default {
 	},
 	mounted() {
 		if (this.$route.query.returnToSong) {
-			this.closeModal({ sector: this.sector, modal: "editSong" });
+			this.closeModal("editSong");
 		}
 
 		this.socket.dispatch(`reports.findOne`, this.reportId, res => {
@@ -109,10 +109,7 @@ export default {
 				this.viewReport(report);
 			} else {
 				new Toast("Report with that ID not found");
-				this.closeModal({
-					sector: this.sector,
-					modal: "viewReport"
-				});
+				this.closeModal("viewReport");
 			}
 		});
 	},
@@ -121,11 +118,7 @@ export default {
 		resolve(reportId) {
 			return this.resolveReport(reportId)
 				.then(res => {
-					if (res.status === "success")
-						this.closeModal({
-							sector: this.sector,
-							modal: "viewReport"
-						});
+					if (res.status === "success") this.closeModal("viewReport");
 				})
 				.catch(err => new Toast(err.message));
 		},

+ 2 - 1
frontend/src/main.js

@@ -184,12 +184,13 @@ lofig.folder = "../config/default.json";
 	ws.init(websocketsDomain);
 
 	ws.socket.on("ready", res => {
-		const { loggedIn, role, username, userId } = res.data;
+		const { loggedIn, role, username, userId, email } = res.data;
 
 		store.dispatch("user/auth/authData", {
 			loggedIn,
 			role,
 			username,
+			email,
 			userId
 		});
 	});

+ 2 - 2
frontend/src/pages/Admin/tabs/HiddenSongs.vue

@@ -204,7 +204,7 @@ export default {
 			);
 		},
 		...mapState("modalVisibility", {
-			modals: state => state.modals.admin
+			modals: state => state.modals
 		}),
 		...mapState("admin/hiddenSongs", {
 			songs: state => state.songs
@@ -238,7 +238,7 @@ export default {
 	methods: {
 		edit(song) {
 			this.editSong(song);
-			this.openModal({ sector: "admin", modal: "editSong" });
+			this.openModal("editSong");
 		},
 		unhide(song) {
 			this.socket.dispatch("songs.unhide", song._id, res => {

+ 2 - 2
frontend/src/pages/Admin/tabs/News.vue

@@ -239,7 +239,7 @@ export default {
 	},
 	computed: {
 		...mapState("modalVisibility", {
-			modals: state => state.modals.admin
+			modals: state => state.modals
 		}),
 		...mapState("admin/news", {
 			news: state => state.news
@@ -309,7 +309,7 @@ export default {
 		},
 		editNewsClick(news) {
 			this.editingNewsId = news._id;
-			this.openModal({ sector: "admin", modal: "editNews" });
+			this.openModal("editNews");
 		},
 		addChange(type) {
 			const change = this.$refs[`new-${type}`].value.trim();

+ 4 - 4
frontend/src/pages/Admin/tabs/Playlists.vue

@@ -81,9 +81,9 @@
 			</table>
 		</div>
 
-		<edit-playlist v-if="modals.admin.editPlaylist" sector="admin" />
-		<report v-if="modals.station.report" />
-		<edit-song v-if="modals.admin.editSong" song-type="songs" />
+		<edit-playlist v-if="modals.editPlaylist" sector="admin" />
+		<report v-if="modals.report" />
+		<edit-song v-if="modals.editSong" song-type="songs" />
 	</div>
 </template>
 
@@ -125,7 +125,7 @@ export default {
 	methods: {
 		edit(playlistId) {
 			this.editPlaylist(playlistId);
-			this.openModal({ sector: "admin", modal: "editPlaylist" });
+			this.openModal("editPlaylist");
 		},
 		init() {
 			this.socket.dispatch("playlists.index", res => {

+ 2 - 2
frontend/src/pages/Admin/tabs/Punishments.vue

@@ -118,7 +118,7 @@ export default {
 			return this.punishments;
 		},
 		...mapState("modalVisibility", {
-			modals: state => state.modals.admin
+			modals: state => state.modals
 		}),
 		...mapGetters({
 			socket: "websockets/getSocket"
@@ -136,7 +136,7 @@ export default {
 		view(punishment) {
 			// this.viewPunishment(punishment);
 			this.viewingPunishmentId = punishment._id;
-			this.openModal({ sector: "admin", modal: "viewPunishment" });
+			this.openModal("viewPunishment");
 		},
 		banIP() {
 			this.socket.dispatch(

+ 3 - 6
frontend/src/pages/Admin/tabs/Reports.vue

@@ -87,7 +87,7 @@ export default {
 	},
 	computed: {
 		...mapState("modalVisibility", {
-			modals: state => state.modals.admin
+			modals: state => state.modals
 		}),
 		...mapGetters({
 			socket: "websockets/getSocket"
@@ -130,16 +130,13 @@ export default {
 		view(report) {
 			// this.viewReport(report);
 			this.viewingReportId = report._id;
-			this.openModal({ sector: "admin", modal: "viewReport" });
+			this.openModal("viewReport");
 		},
 		resolve(reportId) {
 			return this.resolveReport(reportId)
 				.then(res => {
 					if (res.status === "success" && this.modals.viewReport)
-						this.closeModal({
-							sector: "admin",
-							modal: "viewReport"
-						});
+						this.closeModal("viewReport");
 				})
 				.catch(err => new Toast(err.message));
 		},

+ 2 - 5
frontend/src/pages/Admin/tabs/Stations.vue

@@ -214,7 +214,7 @@ export default {
 			stations: state => state.stations
 		}),
 		...mapState("modalVisibility", {
-			modals: state => state.modals.admin
+			modals: state => state.modals
 		}),
 		...mapGetters({
 			socket: "websockets/getSocket"
@@ -282,10 +282,7 @@ export default {
 		},
 		manage(station) {
 			this.editingStationId = station._id;
-			this.openModal({
-				sector: "admin",
-				modal: "manageStation"
-			});
+			this.openModal("manageStation");
 		},
 		addGenre() {
 			const genre = this.$refs["new-genre"].value.toLowerCase().trim();

+ 2 - 2
frontend/src/pages/Admin/tabs/UnverifiedSongs.vue

@@ -220,7 +220,7 @@ export default {
 			);
 		},
 		...mapState("modalVisibility", {
-			modals: state => state.modals.admin
+			modals: state => state.modals
 		}),
 		...mapState("admin/unverifiedSongs", {
 			songs: state => state.songs
@@ -254,7 +254,7 @@ export default {
 	methods: {
 		edit(song) {
 			this.editSong(song);
-			this.openModal({ sector: "admin", modal: "editSong" });
+			this.openModal("editSong");
 		},
 		verify(song) {
 			this.socket.dispatch("songs.verify", song.youtubeId, res => {

+ 2 - 2
frontend/src/pages/Admin/tabs/Users.vue

@@ -84,7 +84,7 @@ export default {
 	},
 	computed: {
 		...mapState("modalVisibility", {
-			modals: state => state.modals.admin
+			modals: state => state.modals
 		}),
 		...mapGetters({
 			socket: "websockets/getSocket"
@@ -97,7 +97,7 @@ export default {
 	methods: {
 		edit(user) {
 			this.editingUserId = user._id;
-			this.openModal({ sector: "admin", modal: "editUser" });
+			this.openModal("editUser");
 		},
 		init() {
 			this.socket.dispatch("users.index", res => {

+ 2 - 2
frontend/src/pages/Admin/tabs/VerifiedSongs.vue

@@ -301,7 +301,7 @@ export default {
 				);
 		},
 		...mapState("modalVisibility", {
-			modals: state => state.modals.admin
+			modals: state => state.modals
 		}),
 		...mapState("admin/verifiedSongs", {
 			songs: state => state.songs
@@ -347,7 +347,7 @@ export default {
 	methods: {
 		edit(song) {
 			this.editSong(song);
-			this.openModal({ sector: "admin", modal: "editSong" });
+			this.openModal("editSong");
 		},
 		remove(id) {
 			// eslint-disable-next-line

+ 5 - 25
frontend/src/pages/Home.vue

@@ -20,23 +20,13 @@
 						<div v-if="!loggedIn" class="buttons">
 							<button
 								class="button login"
-								@click="
-									openModal({
-										sector: 'header',
-										modal: 'login'
-									})
-								"
+								@click="openModal('login')"
 							>
 								Login
 							</button>
 							<button
 								class="button register"
-								@click="
-									openModal({
-										sector: 'header',
-										modal: 'register'
-									})
-								"
+								@click="openModal('register')"
 							>
 								Register
 							</button>
@@ -246,12 +236,7 @@
 				</div>
 				<a
 					v-if="loggedIn"
-					@click="
-						openModal({
-							sector: 'home',
-							modal: 'createCommunityStation'
-						})
-					"
+					@click="openModal('createCommunityStation')"
 					class="card station-card createStation"
 				>
 					<div class="card-image">
@@ -274,12 +259,7 @@
 				</a>
 				<a
 					v-else
-					@click="
-						openModal({
-							sector: 'header',
-							modal: 'login'
-						})
-					"
+					@click="openModal('login')"
 					class="card station-card createStation"
 				>
 					<div class="card-image">
@@ -493,7 +473,7 @@ export default {
 		...mapState({
 			loggedIn: state => state.user.auth.loggedIn,
 			userId: state => state.user.auth.userId,
-			modals: state => state.modalVisibility.modals.home
+			modals: state => state.modalVisibility.modals
 		}),
 		...mapGetters({
 			socket: "websockets/getSocket"

+ 3 - 3
frontend/src/pages/Profile/index.vue

@@ -1,8 +1,8 @@
 <template>
 	<div v-if="isUser">
-		<edit-playlist v-if="modals.station.editPlaylist" />
-		<report v-if="modals.station.report" />
-		<edit-song v-if="modals.admin.editSong" song-type="songs" />
+		<edit-playlist v-if="modals.editPlaylist" />
+		<report v-if="modals.report" />
+		<edit-song v-if="modals.editSong" song-type="songs" />
 
 		<metadata :title="`Profile | ${user.username}`" />
 		<main-header />

+ 3 - 8
frontend/src/pages/Profile/tabs/Playlists.vue

@@ -76,12 +76,7 @@
 				v-if="myUserId === userId"
 				class="button is-primary"
 				id="create-new-playlist-button"
-				@click="
-					openModal({
-						sector: 'station',
-						modal: 'createPlaylist'
-					})
-				"
+				@click="openModal('createPlaylist')"
 			>
 				Create new playlist
 			</button>
@@ -120,7 +115,7 @@ export default {
 	computed: {
 		...mapState({
 			...mapState("modalVisibility", {
-				modals: state => state.modals.station
+				modals: state => state.modals
 			}),
 			myUserId: state => state.user.auth.userId
 		}),
@@ -222,7 +217,7 @@ export default {
 	methods: {
 		showPlaylist(playlistId) {
 			this.editPlaylist(playlistId);
-			this.openModal({ sector: "station", modal: "editPlaylist" });
+			this.openModal("editPlaylist");
 		},
 		...mapActions("modalVisibility", ["openModal"]),
 		...mapActions("user/playlists", ["editPlaylist", "setPlaylists"])

+ 1 - 1
frontend/src/pages/Profile/tabs/RecentActivity.vue

@@ -69,7 +69,7 @@ export default {
 	computed: {
 		...mapState({
 			...mapState("modalVisibility", {
-				modals: state => state.modals.station
+				modals: state => state.modals
 			}),
 			myUserId: state => state.user.auth.userId
 		}),

+ 84 - 195
frontend/src/pages/ResetPassword.vue

@@ -43,9 +43,10 @@
 										type="email"
 										placeholder="Enter email address here..."
 										autofocus
-										v-model="email"
+										v-model="email.value"
 										@keyup.enter="submitEmail()"
 										@keypress="onInput('email')"
+										@paste="onInput('email')"
 									/>
 								</p>
 								<p class="control">
@@ -62,9 +63,9 @@
 							</div>
 							<transition name="fadein-helpbox">
 								<input-help-box
-									:entered="validation.email.entered"
-									:valid="validation.email.valid"
-									:message="validation.email.message"
+									:entered="email.entered"
+									:valid="email.valid"
+									:message="email.message"
 								/>
 							</transition>
 						</div>
@@ -77,16 +78,18 @@
 						</h2>
 						<p
 							class="content-box-description"
-							v-if="!this.hasEmailBeenSentAlready"
+							v-if="!this.email.hasBeenSentAlready"
 						>
 							A code has been sent to
-							<strong>{{ email }}.</strong>
+							<strong>{{ email.value }}.</strong>
 						</p>
 
 						<p class="content-box-optional-helper">
 							<a
 								href="#"
-								@click="email ? submitEmail() : (step = 1)"
+								@click="
+									email.value ? submitEmail() : (step = 1)
+								"
 								>Request another code</a
 							>
 						</p>
@@ -133,16 +136,17 @@
 									id="new-password"
 									type="password"
 									placeholder="Enter password here..."
-									v-model="newPassword"
-									@keypress="onInput('newPassword')"
+									v-model="password.value"
+									@keypress="onInput('password')"
+									@paste="onInput('password')"
 								/>
 							</p>
 
 							<transition name="fadein-helpbox">
 								<input-help-box
-									:entered="validation.newPassword.entered"
-									:valid="validation.newPassword.valid"
-									:message="validation.newPassword.message"
+									:entered="password.entered"
+									:valid="password.valid"
+									:message="password.message"
 								/>
 							</transition>
 
@@ -158,21 +162,18 @@
 									id="new-password-again"
 									type="password"
 									placeholder="Enter password here..."
-									v-model="newPasswordAgain"
+									v-model="passwordAgain.value"
 									@keyup.enter="changePassword()"
-									@keypress="onInput('newPasswordAgain')"
+									@keypress="onInput('passwordAgain')"
+									@paste="onInput('passwordAgain')"
 								/>
 							</p>
 
 							<transition name="fadein-helpbox">
 								<input-help-box
-									:entered="
-										validation.newPasswordAgain.entered
-									"
-									:valid="validation.newPasswordAgain.valid"
-									:message="
-										validation.newPasswordAgain.message
-									"
+									:entered="passwordAgain.entered"
+									:valid="passwordAgain.valid"
+									:message="passwordAgain.message"
 								/>
 							</transition>
 
@@ -229,7 +230,7 @@
 
 <script>
 import Toast from "toasters";
-import { mapGetters } from "vuex";
+import { mapGetters, mapState } from "vuex";
 
 import MainHeader from "@/components/layout/MainHeader.vue";
 import MainFooter from "@/components/layout/MainFooter.vue";
@@ -248,94 +249,98 @@ export default {
 	},
 	data() {
 		return {
-			email: "",
-			hasEmailBeenSentAlready: true,
 			code: "",
-			newPassword: "",
-			newPasswordAgain: "",
-			validation: {
-				email: {
-					entered: false,
-					valid: false,
-					message: "Please enter a valid email address."
-				},
-				newPassword: {
-					entered: false,
-					valid: false,
-					message:
-						"Include at least one lowercase letter, one uppercase letter, one number and one special character."
-				},
-				newPasswordAgain: {
-					entered: false,
-					valid: false,
-					message: "This password must match."
-				}
+			email: {
+				value: "",
+				hasBeenSentAlready: true,
+				entered: false,
+				valid: false,
+				message: "Please enter a valid email address."
+			},
+			password: {
+				value: "",
+				entered: false,
+				valid: false,
+				message:
+					"Include at least one lowercase letter, one uppercase letter, one number and one special character."
+			},
+			passwordAgain: {
+				value: "",
+				entered: false,
+				valid: false,
+				message: "This password must match."
 			},
 			step: 1
 		};
 	},
-	computed: mapGetters({
-		socket: "websockets/getSocket"
-	}),
+	computed: {
+		...mapGetters({
+			socket: "websockets/getSocket"
+		}),
+		...mapState({
+			accountEmail: state => state.user.auth.email
+		})
+	},
 	watch: {
-		email(value) {
+		"email.value": function watchEmail(value) {
 			if (
 				value.indexOf("@") !== value.lastIndexOf("@") ||
 				!validation.regex.emailSimple.test(value)
 			) {
-				this.validation.email.message =
-					"Please enter a valid email address.";
-				this.validation.email.valid = false;
+				this.email.message = "Please enter a valid email address.";
+				this.email.valid = false;
 			} else {
-				this.validation.email.message = "Everything looks great!";
-				this.validation.email.valid = true;
+				this.email.message = "Everything looks great!";
+				this.email.valid = true;
 			}
 		},
-		newPassword(value) {
-			this.checkPasswordMatch(value, this.newPasswordAgain);
+		"password.value": function watchPassword(value) {
+			this.checkPasswordMatch(value, this.passwordAgain.value);
 
 			if (!validation.isLength(value, 6, 200)) {
-				this.validation.newPassword.message =
+				this.password.message =
 					"Password must have between 6 and 200 characters.";
-				this.validation.newPassword.valid = false;
+				this.password.valid = false;
 			} else if (!validation.regex.password.test(value)) {
-				this.validation.newPassword.message =
+				this.password.message =
 					"Include at least one lowercase letter, one uppercase letter, one number and one special character.";
-				this.validation.newPassword.valid = false;
+				this.password.valid = false;
 			} else {
-				this.validation.newPassword.message = "Everything looks great!";
-				this.validation.newPassword.valid = true;
+				this.password.message = "Everything looks great!";
+				this.password.valid = true;
 			}
 		},
-		newPasswordAgain(value) {
-			this.checkPasswordMatch(this.newPassword, value);
+		"passwordAgain.value": function watchPasswordAgain(value) {
+			this.checkPasswordMatch(this.password.value, value);
 		}
 	},
+	mounted() {
+		this.email.value = this.accountEmail;
+	},
 	methods: {
-		checkPasswordMatch(newPassword, newPasswordAgain) {
-			if (newPasswordAgain !== newPassword) {
-				this.validation.newPasswordAgain.message =
-					"This password must match.";
-				this.validation.newPasswordAgain.valid = false;
+		checkPasswordMatch(password, passwordAgain) {
+			if (passwordAgain !== password) {
+				this.passwordAgain.message = "This password must match.";
+				this.passwordAgain.valid = false;
 			} else {
-				this.validation.newPasswordAgain.message =
-					"Everything looks great!";
-				this.validation.newPasswordAgain.valid = true;
+				this.passwordAgain.message = "Everything looks great!";
+				this.passwordAgain.valid = true;
 			}
 		},
 		onInput(inputName) {
-			this.validation[inputName].entered = true;
+			this[inputName].entered = true;
 		},
 		submitEmail() {
 			if (
-				this.email.indexOf("@") !== this.email.lastIndexOf("@") ||
-				!validation.regex.emailSimple.test(this.email)
+				this.email.value.indexOf("@") !==
+					this.email.value.lastIndexOf("@") ||
+				!validation.regex.emailSimple.test(this.email.value)
 			)
 				return new Toast("Invalid email format.");
 
-			if (!this.email) return new Toast("Email cannot be empty");
+			if (!this.email.value) return new Toast("Email cannot be empty");
 
-			this.hasEmailBeenSentAlready = false;
+			this.email.hasBeenSentAlready = false;
 
 			if (this.mode === "set") {
 				return this.socket.dispatch("users.requestPassword", res => {
@@ -346,7 +351,7 @@ export default {
 
 			return this.socket.dispatch(
 				"users.requestPasswordReset",
-				this.email,
+				this.email.value,
 				res => {
 					new Toast(res.message);
 					if (res.status === "success") {
@@ -371,13 +376,10 @@ export default {
 			);
 		},
 		changePassword() {
-			if (
-				this.validation.newPassword.valid &&
-				!this.validation.newPasswordAgain.valid
-			)
+			if (this.password.valid && !this.passwordAgain.valid)
 				return new Toast("Please ensure the passwords match.");
 
-			if (!this.validation.newPassword.valid)
+			if (!this.password.valid)
 				return new Toast("Please enter a valid password.");
 
 			return this.socket.dispatch(
@@ -385,7 +387,7 @@ export default {
 					? "users.changePasswordWithCode"
 					: "users.changePasswordWithResetCode",
 				this.code,
-				this.newPassword,
+				this.password.value,
 				res => {
 					new Toast(res.message);
 					if (res.status === "success") this.step = 4;
@@ -399,11 +401,6 @@ export default {
 
 <style lang="scss" scoped>
 .night-mode {
-	.content-box,
-	.step:not(.selected) {
-		background-color: var(--dark-grey-3) !important;
-	}
-
 	.label {
 		color: var(--light-grey-2);
 	}
@@ -428,99 +425,6 @@ p {
 		text-align: center;
 	}
 
-	#steps {
-		display: flex;
-		align-items: center;
-		justify-content: center;
-		height: 50px;
-		margin-top: 36px;
-
-		@media screen and (max-width: 300px) {
-			display: none;
-		}
-
-		.step {
-			display: flex;
-			align-items: center;
-			justify-content: center;
-			border-radius: 100%;
-			border: 1px solid var(--dark-grey);
-			min-width: 50px;
-			min-height: 50px;
-			background-color: var(--white);
-			font-size: 30px;
-			cursor: pointer;
-
-			&.selected {
-				background-color: var(--primary-color);
-				color: var(--white) !important;
-				border: 0;
-			}
-		}
-
-		.divider {
-			display: flex;
-			justify-content: center;
-			width: 180px;
-			height: 1px;
-			background-color: var(--dark-grey);
-		}
-	}
-
-	.content-box {
-		margin-top: 90px;
-		border-radius: 3px;
-		background-color: var(--white);
-		border: 1px solid var(--dark-grey);
-		max-width: 580px;
-		padding: 40px;
-
-		@media screen and (max-width: 300px) {
-			margin-top: 30px;
-			padding: 30px 20px;
-		}
-
-		.content-box-title {
-			font-size: 25px;
-			color: var(--black);
-		}
-
-		.content-box-description {
-			font-size: 14px;
-			color: var(--dark-grey);
-		}
-
-		.content-box-optional-helper {
-			margin-top: 15px;
-			color: var(--primary-color);
-			text-decoration: underline;
-			font-size: 16px;
-		}
-
-		.content-box-inputs {
-			margin-top: 35px;
-
-			.input-with-button {
-				.button {
-					width: 105px;
-				}
-
-				@media screen and (max-width: 450px) {
-					flex-direction: column;
-				}
-			}
-
-			label {
-				font-size: 11px;
-			}
-
-			#change-password-button {
-				margin-top: 36px;
-				width: 175px;
-			}
-		}
-	}
-
 	.reset-status-box {
 		display: flex;
 		flex-direction: column;
@@ -555,21 +459,6 @@ p {
 	}
 }
 
-.steps-fade-enter-active,
-.steps-fade-leave-active {
-	transition: all 0.3s ease;
-}
-
-.steps-fade-enter,
-.steps-fade-leave-to {
-	opacity: 0;
-}
-
-.skip-step {
-	background-color: var(--grey-3);
-	color: var(--white);
-}
-
 .control {
 	margin-bottom: 2px !important;
 }

+ 19 - 5
frontend/src/pages/Settings/index.vue

@@ -42,11 +42,13 @@
 			</div>
 		</div>
 		<main-footer />
+
+		<remove-account v-if="modals.removeAccount" />
 	</div>
 </template>
 
 <script>
-import { mapActions, mapGetters } from "vuex";
+import { mapActions, mapGetters, mapState } from "vuex";
 import Toast from "toasters";
 
 import MainHeader from "@/components/layout/MainHeader.vue";
@@ -60,7 +62,8 @@ export default {
 		SecuritySettings: () => import("./tabs/Security.vue"),
 		AccountSettings: () => import("./tabs/Account.vue"),
 		ProfileSettings: () => import("./tabs/Profile.vue"),
-		PreferencesSettings: () => import("./tabs/Preferences.vue")
+		PreferencesSettings: () => import("./tabs/Preferences.vue"),
+		RemoveAccount: () => import("@/components/modals/RemoveAccount.vue")
 	},
 	mixins: [TabQueryHandler],
 	data() {
@@ -68,9 +71,14 @@ export default {
 			tab: ""
 		};
 	},
-	computed: mapGetters({
-		socket: "websockets/getSocket"
-	}),
+	computed: {
+		...mapGetters({
+			socket: "websockets/getSocket"
+		}),
+		...mapState("modalVisibility", {
+			modals: state => state.modals
+		})
+	},
 	mounted() {
 		if (
 			this.$route.query.tab === "profile" ||
@@ -131,6 +139,12 @@ export default {
 	margin-top: 32px;
 	padding: 24px;
 
+	/deep/ .row {
+		*:not(:last-child) {
+			margin-right: 5px;
+		}
+	}
+
 	.content {
 		background-color: var(--white);
 		padding: 30px 50px;

+ 14 - 22
frontend/src/pages/Settings/tabs/Account.vue

@@ -17,6 +17,7 @@
 				maxlength="32"
 				autocomplete="off"
 				@keypress="onInput('username')"
+				@paste="onInput('username')"
 			/>
 			<span v-if="modifiedUser.username" class="character-counter"
 				>{{ modifiedUser.username.length }}/32</span
@@ -40,6 +41,7 @@
 				v-if="modifiedUser.email"
 				v-model="modifiedUser.email.address"
 				@keypress="onInput('email')"
+				@paste="onInput('email')"
 				autocomplete="off"
 			/>
 		</p>
@@ -76,17 +78,15 @@
 		<div class="row">
 			<confirm @confirm="removeActivities()">
 				<a class="button is-warning">
-					<i class="material-icons icon-with-button">clear</i>
+					<i class="material-icons icon-with-button">cancel</i>
 					Clear my activities
 				</a>
 			</confirm>
 
-			<confirm @confirm="removeAccount()">
-				<a class="button is-danger">
-					<i class="material-icons icon-with-button">delete</i>
-					Remove my account
-				</a>
-			</confirm>
+			<a class="button is-danger" @click="openModal('removeAccount')">
+				<i class="material-icons icon-with-button">delete</i>
+				Remove my account
+			</a>
 		</div>
 	</div>
 </template>
@@ -101,7 +101,11 @@ import validation from "@/validation";
 import Confirm from "@/components/Confirm.vue";
 
 export default {
-	components: { InputHelpBox, SaveButton, Confirm },
+	components: {
+		InputHelpBox,
+		SaveButton,
+		Confirm
+	},
 	data() {
 		return {
 			validation: {
@@ -259,26 +263,14 @@ export default {
 				}
 			);
 		},
-		removeAccount() {
-			return this.socket.dispatch("users.remove", res => {
-				if (res.status === "success") {
-					return this.socket.dispatch("users.logout", () => {
-						return lofig.get("cookie").then(cookie => {
-							document.cookie = `${cookie.SIDname}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
-							return window.location.reload();
-						});
-					});
-				}
 
-				return new Toast(res.message);
-			});
-		},
 		removeActivities() {
 			this.socket.dispatch("activities.removeAllForUser", res => {
 				new Toast(res.message);
 			});
 		},
-		...mapActions("settings", ["updateOriginalUser"])
+		...mapActions("settings", ["updateOriginalUser"]),
+		...mapActions("modalVisibility", ["openModal"])
 	}
 };
 </script>

+ 2 - 1
frontend/src/pages/Settings/tabs/Security.vue

@@ -30,6 +30,7 @@
 					v-model="validation.newPassword.value"
 					@keyup.enter="changePassword()"
 					@keypress="onInput('newPassword')"
+					@paste="onInput('newPassword')"
 				/>
 			</p>
 
@@ -71,7 +72,7 @@
 		</div>
 
 		<div v-if="!isGithubLinked">
-			<h4 class="section-title">Link GitHub</h4>
+			<h4 class="section-title">Link your GitHub account</h4>
 			<p class="section-description">
 				Link your Musare account with GitHub.
 			</p>

+ 2 - 2
frontend/src/pages/Station/Sidebar/Playlists.vue

@@ -73,7 +73,7 @@
 		<a
 			class="button create-playlist tab-actionable-button"
 			href="#"
-			@click="openModal({ sector: 'station', modal: 'createPlaylist' })"
+			@click="openModal('createPlaylist')"
 		>
 			<i class="material-icons icon-with-button">create</i>
 			<span class="optional-desktop-only-text"> Create Playlist </span>
@@ -193,7 +193,7 @@ export default {
 	methods: {
 		edit(id) {
 			this.editPlaylist(id);
-			this.openModal({ sector: "station", modal: "editPlaylist" });
+			this.openModal("editPlaylist");
 		},
 		selectPlaylist(playlist) {
 			if (this.station.type === "community" && this.station.partyMode) {

+ 7 - 16
frontend/src/pages/Station/index.vue

@@ -156,12 +156,7 @@
 								<!-- (Admin) Station Settings Button -->
 								<button
 									class="button is-primary"
-									@click="
-										openModal({
-											sector: 'station',
-											modal: 'manageStation'
-										})
-									"
+									@click="openModal('manageStation')"
 								>
 									<i class="material-icons icon-with-button"
 										>settings</i
@@ -566,25 +561,21 @@
 					</div>
 				</div>
 
-				<request-song v-if="modals.station.requestSong" />
-				<edit-playlist v-if="modals.station.editPlaylist" />
-				<create-playlist v-if="modals.station.createPlaylist" />
+				<request-song v-if="modals.requestSong" />
+				<edit-playlist v-if="modals.editPlaylist" />
+				<create-playlist v-if="modals.createPlaylist" />
 				<manage-station
-					v-if="modals.station.manageStation"
+					v-if="modals.manageStation"
 					:station-id="station._id"
 					sector="station"
 				/>
-				<report v-if="modals.station.report" />
+				<report v-if="modals.report" />
 			</div>
 
 			<main-footer v-if="exists" />
 		</div>
 
-		<edit-song
-			v-if="modals.admin.editSong"
-			song-type="songs"
-			sector="station"
-		/>
+		<edit-song v-if="modals.editSong" song-type="songs" sector="station" />
 
 		<floating-box id="player-debug-box" ref="playerDebugBox">
 			<template #body>

+ 112 - 96
frontend/src/pages/Team.vue

@@ -2,117 +2,125 @@
 	<div class="app">
 		<metadata title="Team" />
 		<main-header />
-		<h2 class="has-text-centered">Current Team</h2>
-		<div class="group">
-			<div
-				v-for="(member, index) in currentTeam"
-				:key="'current-' + index"
-				class="card"
-			>
-				<header class="card-header">
-					<profile-picture
-						:avatar="member.avatar"
-						:name="member.name"
-					/>
-					<div>
-						<strong>{{ member.name }}</strong>
-						<span v-if="member.active"
-							>Active: {{ member.active }}</span
-						>
-					</div>
-					<a
-						v-if="member.link"
-						:href="member.link"
-						target="_blank"
-						class="material-icons"
-					>
-						link
-					</a>
-				</header>
-				<div class="card-content">
-					<div v-if="member.bio" class="bio">
-						{{ member.bio }}
-					</div>
-					<div v-if="member.projects" class="projects">
+		<div class="container">
+			<h2 class="has-text-centered">Current Team</h2>
+			<div class="group">
+				<div
+					v-for="(member, index) in currentTeam"
+					:key="index"
+					class="card"
+				>
+					<header class="card-header">
+						<profile-picture
+							:avatar="member.avatar"
+							:name="member.name"
+						/>
+						<div>
+							<strong>{{ member.name }}</strong>
+							<span v-if="member.active"
+								>Active: {{ member.active }}</span
+							>
+						</div>
 						<a
-							v-for="(project, pindex) in member.projects"
-							:key="'currentp-' + pindex"
-							:href="
-								'https://github.com/Musare/' +
-									project +
-									'/commits?author=' +
-									member.github
-							"
+							v-if="member.link"
+							:href="member.link"
 							target="_blank"
+							class="material-icons"
 						>
-							{{ project }}
+							link
 						</a>
+					</header>
+					<div class="card-content">
+						<div
+							v-if="member.bio"
+							class="bio"
+							v-html="member.bio"
+						></div>
+						<div v-if="member.projects" class="projects">
+							<a
+								v-for="(project,
+								projectIndex) in member.projects"
+								:key="projectIndex"
+								:href="
+									'https://github.com/Musare/' +
+										project +
+										'/commits?author=' +
+										member.github
+								"
+								target="_blank"
+							>
+								{{ project }}
+							</a>
+						</div>
 					</div>
 				</div>
 			</div>
-		</div>
-		<h3 class="has-text-centered">Previous Team</h3>
-		<div class="group">
-			<div
-				v-for="(member, index) in previousTeam"
-				:key="'previous-' + index"
-				class="card"
-			>
-				<header class="card-header">
-					<profile-picture
-						:avatar="{ type: 'text', color: 'grey' }"
-						:name="member.name"
-					/>
-					<div>
-						<strong>{{ member.name }}</strong>
-						<span v-if="member.active"
-							>Active: {{ member.active }}</span
+			<h3 class="has-text-centered">Previous Team</h3>
+			<div class="group">
+				<div
+					v-for="(member, index) in previousTeam"
+					:key="index"
+					class="card"
+				>
+					<header class="card-header">
+						<profile-picture
+							:avatar="{ type: 'text', color: 'grey' }"
+							:name="member.name"
+						/>
+						<div>
+							<strong>{{ member.name }}</strong>
+							<span v-if="member.active"
+								>Active: {{ member.active }}</span
+							>
+						</div>
+						<a
+							v-if="member.link"
+							:href="member.link"
+							target="_blank"
+							class="material-icons"
 						>
+							link
+						</a>
+					</header>
+					<div class="card-content">
+						<div
+							v-if="member.bio"
+							class="bio"
+							v-html="member.bio"
+						></div>
+						<div v-if="member.projects" class="projects">
+							<a
+								v-for="(project,
+								projectIndex) in member.projects"
+								:key="projectIndex"
+								:href="
+									'https://github.com/Musare/' +
+										project +
+										'/commits?author=' +
+										member.github
+								"
+								target="_blank"
+							>
+								{{ project }}
+							</a>
+						</div>
 					</div>
+				</div>
+			</div>
+			<div class="other-contributors">
+				<h4>Other Contributors</h4>
+				<div>
 					<a
-						v-if="member.link"
+						v-for="(member, index) in otherContributors"
+						:key="index"
 						:href="member.link"
 						target="_blank"
-						class="material-icons"
 					>
-						link
+						{{ member.name }}
 					</a>
-				</header>
-				<div class="card-content">
-					<div v-if="member.bio" class="bio">
-						{{ member.bio }}
-					</div>
-					<div v-if="member.projects" class="projects">
-						<a
-							v-for="(project, pindex) in member.projects"
-							:key="'previousp-' + pindex"
-							:href="
-								'https://github.com/Musare/' +
-									project +
-									'/commits?author=' +
-									member.github
-							"
-							target="_blank"
-						>
-							{{ project }}
-						</a>
-					</div>
 				</div>
 			</div>
 		</div>
-		<div class="other-contributors">
-			<h4>Other Contributors</h4>
-			<div>
-				<a
-					v-for="(member, index) in otherContributors"
-					:key="index"
-					:href="member.link"
-					target="_blank"
-				>
-					{{ member.name }}
-				</a>
-			</div>
-		</div>
 		<main-footer />
 	</div>
 </template>
@@ -168,7 +176,11 @@ export default {
 				},
 				{
 					name: "Jonathan Graham",
-					bio: "Lead Developer, Designer and QA Tester.",
+					bio: `
+						<em>Gap-year student based in the UK hoping to study Computer Science at university in September.</em>
+						<br /><br />
+						Lead Developer, Designer and QA Tester.
+						`,
 					projects: [
 						"MusareMeteor",
 						"MusareReact",
@@ -256,6 +268,10 @@ export default {
 	}
 }
 
+.container {
+	max-width: 100% !important;
+}
+
 a {
 	color: var(--primary-color);
 	&:hover,
@@ -355,7 +371,7 @@ h2 {
 				margin-top: auto;
 
 				a {
-					background: var(--light-grey-2);
+					background: var(--light-grey);
 					height: 30px;
 					padding: 5px;
 					border-radius: 5px;

+ 25 - 36
frontend/src/store/modules/modalVisibility.js

@@ -2,29 +2,20 @@
 
 const state = {
 	modals: {
-		header: {
-			login: false,
-			register: false
-		},
-		home: {
-			createCommunityStation: false
-		},
-		station: {
-			requestSong: false,
-			editPlaylist: false,
-			createPlaylist: false,
-			manageStation: false,
-			report: false
-		},
-		admin: {
-			editNews: false,
-			editUser: false,
-			editSong: false,
-			manageStation: false,
-			editPlaylist: false,
-			viewReport: false,
-			viewPunishment: false
-		}
+		manageStation: false,
+		login: false,
+		register: false,
+		createCommunityStation: false,
+		requestSong: false,
+		editPlaylist: false,
+		createPlaylist: false,
+		report: false,
+		removeAccount: false,
+		editNews: false,
+		editUser: false,
+		editSong: false,
+		viewReport: false,
+		viewPunishment: false
 	},
 	currentlyActive: []
 };
@@ -32,17 +23,17 @@ const state = {
 const getters = {};
 
 const actions = {
-	closeModal: ({ commit }, data) => {
-		if (data.modal === "register")
+	closeModal: ({ commit }, modal) => {
+		if (modal === "register")
 			lofig.get("recaptcha.enabled").then(enabled => {
 				if (enabled) window.location.reload();
 			});
 
-		commit("closeModal", data);
+		commit("closeModal", modal);
 		commit("closeCurrentModal");
 	},
-	openModal: ({ commit }, data) => {
-		commit("openModal", data);
+	openModal: ({ commit }, modal) => {
+		commit("openModal", modal);
 	},
 	closeCurrentModal: ({ commit }) => {
 		commit("closeCurrentModal");
@@ -50,17 +41,15 @@ const actions = {
 };
 
 const mutations = {
-	closeModal(state, data) {
-		state.modals[data.sector][data.modal] = false;
+	closeModal(state, modal) {
+		state.modals[modal] = false;
 	},
-	openModal(state, data) {
-		state.modals[data.sector][data.modal] = true;
-		state.currentlyActive.unshift(data);
+	openModal(state, modal) {
+		state.modals[modal] = true;
+		state.currentlyActive.unshift(modal);
 	},
 	closeCurrentModal(state) {
-		const { sector, modal } = state.currentlyActive[0];
-		state.modals[sector][modal] = false;
-
+		state.modals[state.currentlyActive[0]] = false;
 		state.currentlyActive.shift();
 	}
 };

+ 2 - 0
frontend/src/store/modules/user.js

@@ -20,6 +20,7 @@ const modules = {
 			loggedIn: false,
 			role: "",
 			username: "",
+			email: "",
 			userId: "",
 			banned: false,
 			ban: {},
@@ -185,6 +186,7 @@ const modules = {
 				state.loggedIn = data.loggedIn;
 				state.role = data.role;
 				state.username = data.username;
+				state.email = data.email;
 				state.userId = data.userId;
 				state.gotData = true;
 			},