Owen Diffey 5 years ago
parent
commit
10d2e1db64

+ 0 - 1
.gitignore

@@ -24,7 +24,6 @@ frontend/bundle-stats.json
 frontend/bundle-report.html
 frontend/node_modules/
 frontend/dist/build/
-!frontend/dist/lofig.min.js
 frontend/dist/index.html
 frontend/dist/config/default.json
 

+ 1 - 0
backend/logic/actions/queueSongs.js

@@ -159,6 +159,7 @@ let lib = {
 				if (song) return next('This song has already been added.');
 				//TODO Add err object as first param of callback
 				utils.getSongFromYouTube(songId, (song) => {
+					song.duration = -1;
 					song.artists = [];
 					song.genres = [];
 					song.skipDuration = 0;

+ 2 - 2
backend/logic/actions/stations.js

@@ -560,10 +560,10 @@ module.exports = {
 		], async (err) => {
 			if (err) {
 				err = await utils.getError(err);
-				logger.error("STATIONS_UPDATE_DISPLAY_NAME", `Updating station "${stationId}" displayName to "${newName}" failed. "${err}"`);
+				logger.error("STATIONS_UPDATE_NAME", `Updating station "${stationId}" name to "${newName}" failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
-			logger.success("STATIONS_UPDATE_DISPLAY_NAME", `Updated station "${stationId}" displayName to "${newName}" successfully.`);
+			logger.success("STATIONS_UPDATE_NAME", `Updated station "${stationId}" name to "${newName}" successfully.`);
 			return cb({'status': 'success', 'message': 'Successfully updated the name.'});
 		});
 	}),

+ 33 - 26
backend/logic/db/index.js

@@ -9,8 +9,8 @@ const regex = {
 	azAZ09_: /^[A-Za-z0-9_]+$/,
 	az09_: /^[a-z0-9_]+$/,
 	emailSimple: /^[\x00-\x7F]+@[a-z0-9]+\.[a-z0-9]+(\.[a-z0-9]+)?$/,
-	password: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!%*?&])[A-Za-z\d$@$!%*?&]/,
-	ascii: /^[\x00-\x7F]+$/
+	ascii: /^[\x00-\x7F]+$/,
+	custom: regex => new RegExp(`^[${regex}]+$`)
 };
 
 const isLength = (string, min, max) => {
@@ -80,22 +80,24 @@ module.exports = class extends coreClass {
 						this._lockdown();
 					});
 		
-					// this.schemas.user.path('username').validate((username) => {
-					// 	return (isLength(username, 2, 32) && regex.azAZ09_.test(username));
-					// }, 'Invalid username.');
+					// User
+					this.schemas.user.path('username').validate((username) => {
+						return (isLength(username, 2, 32) && regex.custom("a-zA-Z0-9_-").test(username));
+					}, 'Invalid username.');
 		
 					this.schemas.user.path('email.address').validate((email) => {
 						if (!isLength(email, 3, 254)) return false;
 						if (email.indexOf('@') !== email.lastIndexOf('@')) return false;
-						return regex.emailSimple.test(email);
+						return regex.emailSimple.test(email) && regex.ascii.test(email);
 					}, 'Invalid email.');
-		
+
+					// Station
 					this.schemas.station.path('name').validate((id) => {
 						return (isLength(id, 2, 16) && regex.az09_.test(id));
 					}, 'Invalid station name.');
 		
 					this.schemas.station.path('displayName').validate((displayName) => {
-						return (isLength(displayName, 2, 32) && regex.azAZ09_.test(displayName));
+						return (isLength(displayName, 2, 32) && regex.ascii.test(displayName));
 					}, 'Invalid display name.');
 		
 					this.schemas.station.path('description').validate((description) => {
@@ -106,7 +108,6 @@ module.exports = class extends coreClass {
 						}).length === 0;
 					}, 'Invalid display name.');
 		
-		
 					this.schemas.station.path('owner').validate({
 						isAsync: true,
 						validator: (owner, callback) => {
@@ -153,7 +154,9 @@ module.exports = class extends coreClass {
 						return callback(false);
 					}, 'The max amount of songs per user is 3, and only 2 in a row is allowed.');
 					*/
-		
+
+
+					// Song
 					let songTitle = (title) => {
 						return isLength(title, 1, 100);
 					};
@@ -169,29 +172,33 @@ module.exports = class extends coreClass {
 		
 					let songArtists = (artists) => {
 						return artists.filter((artist) => {
-								return (isLength(artist, 1, 32) && regex.ascii.test(artist) && artist !== "NONE");
+								return (isLength(artist, 1, 64) && artist !== "NONE");
 							}).length === artists.length;
 					};
 					this.schemas.song.path('artists').validate(songArtists, 'Invalid artists.');
 					this.schemas.queueSong.path('artists').validate(songArtists, 'Invalid artists.');
 		
-					/*let songGenres = (genres) => {
+					let songGenres = (genres) => {
+						if (genres.length < 1 || genres.length > 16) return false;
 						return genres.filter((genre) => {
-								return (isLength(genre, 1, 16) && regex.azAZ09_.test(genre));
+								return (isLength(genre, 1, 32) && regex.ascii.test(genre));
 							}).length === genres.length;
 					};
 					this.schemas.song.path('genres').validate(songGenres, 'Invalid genres.');
-					this.schemas.queueSong.path('genres').validate(songGenres, 'Invalid genres.');*/
-		
-					this.schemas.song.path('thumbnail').validate((thumbnail) => {
-						return isLength(thumbnail, 8, 256);
-					}, 'Invalid thumbnail.');
-					this.schemas.queueSong.path('thumbnail').validate((thumbnail) => {
-						return isLength(thumbnail, 0, 256);
-					}, 'Invalid thumbnail.');
+					this.schemas.queueSong.path('genres').validate(songGenres, 'Invalid genres.');
 		
+					let songThumbnail = (thumbnail) => {
+						if (!isLength(thumbnail, 1, 256)) return false;
+						let startWith = "https://";
+						if (config.get("cookie.secure") === false) startWith = "http://";
+						return thumbnail.startsWith(startWith);
+					};
+					this.schemas.song.path('thumbnail').validate(songThumbnail, 'Invalid thumbnail.');
+					this.schemas.queueSong.path('thumbnail').validate(songThumbnail, 'Invalid thumbnail.');
+
+					// Playlist
 					this.schemas.playlist.path('displayName').validate((displayName) => {
-						return (isLength(displayName, 1, 16) && regex.ascii.test(displayName));
+						return (isLength(displayName, 1, 32) && regex.ascii.test(displayName));
 					}, 'Invalid display name.');
 		
 					this.schemas.playlist.path('createdBy').validate((createdBy) => {
@@ -201,14 +208,15 @@ module.exports = class extends coreClass {
 					}, 'Max 10 playlists per user.');
 		
 					this.schemas.playlist.path('songs').validate((songs) => {
-						return songs.length <= 2000;
-					}, 'Max 2000 songs per playlist.');
+						return songs.length <= 5000;
+					}, 'Max 5000 songs per playlist.');
 		
 					this.schemas.playlist.path('songs').validate((songs) => {
 						if (songs.length === 0) return true;
 						return songs[0].duration <= 10800;
 					}, 'Max 3 hours per song.');
 		
+					// Report
 					this.schemas.report.path('description').validate((description) => {
 						return (!description || (isLength(description, 0, 400) && regex.ascii.test(description)));
 					}, 'Invalid description.');
@@ -223,7 +231,6 @@ module.exports = class extends coreClass {
 	}
 
 	passwordValid(password) {
-		if (!isLength(password, 6, 200)) return false;
-		return regex.password.test(password);
+		return isLength(password, 6, 200);
 	}
 }

+ 4 - 4
backend/logic/utils.js

@@ -352,7 +352,7 @@ module.exports = class extends coreClass {
 
 				body = JSON.parse(body);
 
-				//TODO Clean up duration converter
+				/*//TODO Clean up duration converter
   				let dur = body.items[0].contentDetails.duration;
 				dur = dur.replace('PT', '');
 				let duration = 0;
@@ -370,12 +370,12 @@ module.exports = class extends coreClass {
 					v2 = Number(v2);
 					duration += v2;
 					return '';
-				});
+				});*/
 
 				let song = {
 					songId: body.items[0].id,
-					title: body.items[0].snippet.title,
-					duration
+					title: body.items[0].snippet.title/*,
+					duration*/
 				};
 				cb(song);
 			});

+ 5 - 2
frontend/App.vue

@@ -65,6 +65,7 @@ export default {
 			this.$router.go(localStorage.getItem("github_redirect"));
 			localStorage.removeItem("github_redirect");
 		}
+
 		io.onConnect(true, () => {
 			this.socketConnected = true;
 		});
@@ -74,9 +75,11 @@ export default {
 		io.onDisconnect(true, () => {
 			this.socketConnected = false;
 		});
-		lofig.get("serverDomain", res => {
-			this.serverDomain = res;
+
+		lofig.get("serverDomain").then(serverDomain => {
+			this.serverDomain = serverDomain;
 		});
+
 		this.$router.onReady(() => {
 			if (this.$route.query.err) {
 				let { err } = this.$route.query;

+ 3 - 3
frontend/api/auth.js

@@ -18,7 +18,7 @@ export default {
 					res => {
 						if (res.status === "success") {
 							if (res.SID) {
-								return lofig.get("cookie", cookie => {
+								return lofig.get("cookie").then(cookie => {
 									const date = new Date();
 									date.setTime(
 										new Date().getTime() +
@@ -52,7 +52,7 @@ export default {
 			io.getSocket(socket => {
 				socket.emit("users.login", email, password, res => {
 					if (res.status === "success") {
-						return lofig.get("cookie", cookie => {
+						return lofig.get("cookie").then(cookie => {
 							const date = new Date();
 							date.setTime(
 								new Date().getTime() +
@@ -79,7 +79,7 @@ export default {
 			io.getSocket(socket => {
 				socket.emit("users.logout", result => {
 					if (result.status === "success") {
-						return lofig.get("cookie", cookie => {
+						return lofig.get("cookie").then(cookie => {
 							document.cookie = `${cookie.SIDname}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
 							return window.location.reload();
 						});

+ 0 - 501
frontend/components/Admin/EditStation.vue

@@ -1,501 +0,0 @@
-<template>
-	<modal title="Edit Station">
-		<template v-slot:body>
-			<label class="label">Name</label>
-			<p class="control">
-				<input
-					v-model="editing.name"
-					class="input"
-					type="text"
-					placeholder="Station Name"
-				/>
-			</p>
-			<label class="label">Display name</label>
-			<p class="control">
-				<input
-					v-model="editing.displayName"
-					class="input"
-					type="text"
-					placeholder="Station Display Name"
-				/>
-			</p>
-			<label class="label">Description</label>
-			<p class="control">
-				<input
-					v-model="editing.description"
-					class="input"
-					type="text"
-					placeholder="Station Description"
-				/>
-			</p>
-			<label class="label">Privacy</label>
-			<p class="control">
-				<span class="select">
-					<select v-model="editing.privacy">
-						<option value="public">Public</option>
-						<option value="unlisted">Unlisted</option>
-						<option value="private">Private</option>
-					</select>
-				</span>
-			</p>
-			<br />
-			<p class="control" v-if="station.type === 'community'">
-				<label class="checkbox party-mode-inner">
-					<input v-model="editing.partyMode" type="checkbox" />
-					&nbsp;Party mode
-				</label>
-			</p>
-			<small v-if="station.type === 'community'"
-				>With party mode enabled, people can add songs to a queue that
-				plays. With party mode disabled you can play a private playlist
-				on loop.</small
-			>
-			<br />
-			<div v-if="station.type === 'community' && station.partyMode">
-				<br />
-				<br />
-				<label class="label">Queue lock</label>
-				<small v-if="station.partyMode"
-					>With the queue locked, only owners (you) can add songs to
-					the queue.</small
-				>
-				<br />
-				<button
-					v-if="!station.locked"
-					class="button is-danger"
-					@click="$parent.toggleLock()"
-				>
-					Lock the queue
-				</button>
-				<button
-					v-if="station.locked"
-					class="button is-success"
-					@click="$parent.toggleLock()"
-				>
-					Unlock the queue
-				</button>
-			</div>
-			<div
-				v-if="station.type === 'official'"
-				class="control is-grouped genre-wrapper"
-			>
-				<div class="sector">
-					<p class="control has-addons">
-						<input
-							id="new-genre-edit"
-							class="input"
-							type="text"
-							placeholder="Genre"
-							@keyup.enter="addGenre()"
-						/>
-						<a class="button is-info" href="#" @click="addGenre()"
-							>Add genre</a
-						>
-					</p>
-					<span
-						v-for="(genre, index) in editing.genres"
-						:key="index"
-						class="tag is-info"
-					>
-						{{ genre }}
-						<button
-							class="delete is-info"
-							@click="removeGenre(index)"
-						/>
-					</span>
-				</div>
-				<div class="sector">
-					<p class="control has-addons">
-						<input
-							id="new-blacklisted-genre-edit"
-							class="input"
-							type="text"
-							placeholder="Blacklisted Genre"
-							@keyup.enter="addBlacklistedGenre()"
-						/>
-						<a
-							class="button is-info"
-							href="#"
-							@click="addBlacklistedGenre()"
-							>Add blacklisted genre</a
-						>
-					</p>
-					<span
-						v-for="(genre, index) in editing.blacklistedGenres"
-						:key="index"
-						class="tag is-info"
-					>
-						{{ genre }}
-						<button
-							class="delete is-info"
-							@click="removeBlacklistedGenre(index)"
-						/>
-					</span>
-				</div>
-			</div>
-		</template>
-		<template v-slot:footer>
-			<button class="button is-success" v-on:click="update()">
-				Update Settings
-			</button>
-			<button
-				v-if="station.type === 'community'"
-				class="button is-danger"
-				@click="deleteStation()"
-			>
-				Delete station
-			</button>
-		</template>
-	</modal>
-</template>
-
-<script>
-import { mapState, mapActions } from "vuex";
-import { Toast } from "vue-roaster";
-
-import Modal from "../Modals/Modal.vue";
-import io from "../../io";
-import validation from "../../validation";
-
-export default {
-	computed: mapState("admin/stations", {
-		station: state => state.station,
-		editing: state => state.editing
-	}),
-	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-			return socket;
-		});
-	},
-	methods: {
-		update() {
-			if (this.station.name !== this.editing.name) this.updateName();
-			if (this.station.displayName !== this.editing.displayName)
-				this.updateDisplayName();
-			if (this.station.description !== this.editing.description)
-				this.updateDescription();
-			if (this.station.privacy !== this.editing.privacy)
-				this.updatePrivacy();
-			if (this.station.partyMode !== this.editing.partyMode)
-				this.updatePartyMode();
-			if (
-				this.station.genres.toString() !==
-				this.editing.genres.toString()
-			)
-				this.updateGenres();
-			if (
-				this.station.blacklistedGenres.toString() !==
-				this.editing.blacklistedGenres.toString()
-			)
-				this.updateBlacklistedGenres();
-		},
-		updateName() {
-			const { name } = this.editing;
-			if (!validation.isLength(name, 2, 16))
-				return Toast.methods.addToast(
-					"Name must have between 2 and 16 characters.",
-					8000
-				);
-			if (!validation.regex.az09_.test(name))
-				return Toast.methods.addToast(
-					"Invalid name format. Allowed characters: a-z, 0-9 and _.",
-					8000
-				);
-
-			return this.socket.emit(
-				"stations.updateName",
-				this.editing._id,
-				name,
-				res => {
-					if (res.status === "success") {
-						if (this.station) this.station.name = name;
-						this.$parent.stations.forEach((station, index) => {
-							if (station._id === this.editing._id) {
-								this.$parent.stations[index].name = name;
-								return name;
-							}
-
-							return false;
-						});
-					}
-					Toast.methods.addToast(res.message, 8000);
-				}
-			);
-		},
-		updateDisplayName() {
-			const { displayName } = this.editing;
-			if (!validation.isLength(displayName, 2, 32))
-				return Toast.methods.addToast(
-					"Display name must have between 2 and 32 characters.",
-					8000
-				);
-			if (!validation.regex.azAZ09_.test(displayName))
-				return Toast.methods.addToast(
-					"Invalid display name format. Allowed characters: a-z, A-Z, 0-9 and _.",
-					8000
-				);
-
-			return this.socket.emit(
-				"stations.updateDisplayName",
-				this.editing._id,
-				displayName,
-				res => {
-					if (res.status === "success") {
-						if (this.station) {
-							this.station.displayName = displayName;
-							return displayName;
-						}
-						this.$parent.stations.forEach((station, index) => {
-							if (station._id === this.editing._id) {
-								this.$parent.stations[
-									index
-								].displayName = displayName;
-								return displayName;
-							}
-
-							return false;
-						});
-					}
-
-					return Toast.methods.addToast(res.message, 8000);
-				}
-			);
-		},
-		updateDescription() {
-			const { description } = this.editing;
-			if (!validation.isLength(description, 2, 200))
-				return Toast.methods.addToast(
-					"Description must have between 2 and 200 characters.",
-					8000
-				);
-			let characters = description.split("");
-			characters = characters.filter(character => {
-				return character.charCodeAt(0) === 21328;
-			});
-			if (characters.length !== 0)
-				return Toast.methods.addToast(
-					"Invalid description format. Swastika's are not allowed.",
-					8000
-				);
-
-			return this.socket.emit(
-				"stations.updateDescription",
-				this.editing._id,
-				description,
-				res => {
-					if (res.status === "success") {
-						if (this.station) {
-							this.station.description = description;
-							return description;
-						}
-						this.$parent.stations.forEach((station, index) => {
-							if (station._id === this.editing._id) {
-								this.$parent.stations[
-									index
-								].description = description;
-								return description;
-							}
-
-							return false;
-						});
-
-						return Toast.methods.addToast(res.message, 4000);
-					}
-
-					return Toast.methods.addToast(res.message, 8000);
-				}
-			);
-		},
-		updatePrivacy() {
-			this.socket.emit(
-				"stations.updatePrivacy",
-				this.editing._id,
-				this.editing.privacy,
-				res => {
-					if (res.status === "success") {
-						if (this.station)
-							this.station.privacy = this.editing.privacy;
-						else {
-							this.$parent.stations.forEach((station, index) => {
-								if (station._id === this.editing._id) {
-									this.$parent.stations[
-										index
-									].privacy = this.editing.privacy;
-									return this.editing.privacy;
-								}
-
-								return false;
-							});
-						}
-						return Toast.methods.addToast(res.message, 4000);
-					}
-
-					return Toast.methods.addToast(res.message, 8000);
-				}
-			);
-		},
-		updateGenres() {
-			this.socket.emit(
-				"stations.updateGenres",
-				this.editing._id,
-				this.editing.genres,
-				res => {
-					if (res.status === "success") {
-						const genres = JSON.parse(
-							JSON.stringify(this.editing.genres)
-						);
-						if (this.station) this.station.genres = genres;
-						this.$parent.stations.forEach((station, index) => {
-							if (station._id === this.editing._id) {
-								this.$parent.stations[index].genres = genres;
-								return genres;
-							}
-
-							return false;
-						});
-						return Toast.methods.addToast(res.message, 4000);
-					}
-
-					return Toast.methods.addToast(res.message, 8000);
-				}
-			);
-		},
-		updateBlacklistedGenres() {
-			this.socket.emit(
-				"stations.updateBlacklistedGenres",
-				this.editing._id,
-				this.editing.blacklistedGenres,
-				res => {
-					if (res.status === "success") {
-						const blacklistedGenres = JSON.parse(
-							JSON.stringify(this.editing.blacklistedGenres)
-						);
-						if (this.station)
-							this.station.blacklistedGenres = blacklistedGenres;
-						this.$parent.stations.forEach((station, index) => {
-							if (station._id === this.editing._id) {
-								this.$parent.stations[
-									index
-								].blacklistedGenres = blacklistedGenres;
-								return blacklistedGenres;
-							}
-
-							return false;
-						});
-						return Toast.methods.addToast(res.message, 4000);
-					}
-
-					return Toast.methods.addToast(res.message, 8000);
-				}
-			);
-		},
-		updatePartyMode() {
-			this.socket.emit(
-				"stations.updatePartyMode",
-				this.editing._id,
-				this.editing.partyMode,
-				res => {
-					if (res.status === "success") {
-						if (this.station)
-							this.station.partyMode = this.editing.partyMode;
-						this.$parent.stations.forEach((station, index) => {
-							if (station._id === this.editing._id) {
-								this.$parent.stations[
-									index
-								].partyMode = this.editing.partyMode;
-								return this.editing.partyMode;
-							}
-
-							return false;
-						});
-
-						return Toast.methods.addToast(res.message, 4000);
-					}
-
-					return Toast.methods.addToast(res.message, 8000);
-				}
-			);
-		},
-		addGenre() {
-			const genre = document
-				.getElementById(`new-genre-edit`)
-				.value.toLowerCase()
-				.trim();
-
-			if (this.editing.genres.indexOf(genre) !== -1)
-				return Toast.methods.addToast("Genre already exists", 3000);
-			if (genre) {
-				this.editing.genres.push(genre);
-				document.getElementById(`new-genre`).value = "";
-				return true;
-			}
-			return Toast.methods.addToast("Genre cannot be empty", 3000);
-		},
-		removeGenre(index) {
-			this.editing.genres.splice(index, 1);
-		},
-		addBlacklistedGenre() {
-			const genre = document
-				.getElementById(`new-blacklisted-genre-edit`)
-				.value.toLowerCase()
-				.trim();
-			if (this.editing.blacklistedGenres.indexOf(genre) !== -1)
-				return Toast.methods.addToast("Genre already exists", 3000);
-
-			if (genre) {
-				this.editing.blacklistedGenres.push(genre);
-				document.getElementById(`new-blacklisted-genre`).value = "";
-				return true;
-			}
-			return Toast.methods.addToast("Genre cannot be empty", 3000);
-		},
-		removeBlacklistedGenre(index) {
-			this.editing.blacklistedGenres.splice(index, 1);
-		},
-		deleteStation() {
-			this.socket.emit("stations.remove", this.editing._id, res => {
-				if (res.status === "success")
-					this.closeModal({
-						sector: "station",
-						modal: "editStation"
-					});
-				return Toast.methods.addToast(res.message, 8000);
-			});
-		},
-		...mapActions("modals", ["closeModal"])
-	},
-	components: { Modal }
-};
-</script>
-
-<style lang="scss" scoped>
-@import "scss/variables/colors.scss";
-
-.controls {
-	display: flex;
-
-	a {
-		display: flex;
-		align-items: center;
-	}
-}
-
-.table {
-	margin-bottom: 0;
-}
-
-h5 {
-	padding: 20px 0;
-}
-
-.party-mode-inner,
-.party-mode-outer {
-	display: flex;
-	align-items: center;
-}
-
-.select:after {
-	border-color: $primary-color;
-}
-</style>

+ 2 - 2
frontend/components/Admin/Stations.vue

@@ -177,7 +177,7 @@
 			</div>
 		</div>
 
-		<edit-station v-if="modals.editStation" />
+		<edit-station v-if="modals.editStation" store="admin/stations" />
 	</div>
 </template>
 
@@ -187,7 +187,7 @@ import { mapState, mapActions } from "vuex";
 import { Toast } from "vue-roaster";
 import io from "../../io";
 
-import EditStation from "./EditStation.vue";
+import EditStation from "../Modals/EditStation.vue";
 import UserIdToUsername from "../UserIdToUsername.vue";
 
 export default {

+ 2 - 2
frontend/components/MainFooter.vue

@@ -74,8 +74,8 @@ export default {
 		};
 	},
 	mounted() {
-		lofig.get("siteSettings.socialLinks", res => {
-			this.socialLinks = res;
+		lofig.get("siteSettings.socialLinks").then(socialLinks => {
+			this.socialLinks = socialLinks;
 		});
 	}
 };

+ 5 - 6
frontend/components/MainHeader.vue

@@ -85,13 +85,12 @@ export default {
 		};
 	},
 	mounted() {
-		lofig.get("frontendDomain", res => {
-			this.frontendDomain = res;
-			return res;
+		lofig.get("frontendDomain").then(frontendDomain => {
+			this.frontendDomain = frontendDomain;
 		});
-		lofig.get("siteSettings", res => {
-			this.siteSettings = res;
-			return res;
+
+		lofig.get("siteSettings").then(siteSettings => {
+			this.siteSettings = siteSettings;
 		});
 	},
 	computed: mapState({

+ 3 - 3
frontend/components/Modals/CreateCommunityStation.vue

@@ -89,9 +89,9 @@ export default {
 					"Display name must have between 2 and 32 characters.",
 					8000
 				);
-			if (!validation.regex.azAZ09_.test(displayName))
+			if (!validation.regex.ascii.test(displayName))
 				return Toast.methods.addToast(
-					"Invalid display name format. Allowed characters: a-z, A-Z, 0-9 and _.",
+					"Invalid display name format. Only ASCII characters are allowed.",
 					8000
 				);
 
@@ -109,7 +109,7 @@ export default {
 
 			if (characters.length !== 0)
 				return Toast.methods.addToast(
-					"Invalid description format. Swastika's are not allowed.",
+					"Invalid description format.",
 					8000
 				);
 

+ 40 - 33
frontend/components/Modals/EditSong.vue

@@ -84,20 +84,24 @@
 										class="button album-get-button"
 										v-on:click="getAlbumData('title')"
 									>
-										<i class="material-icons album-get-icon"
-											>album</i
-										>
+										<i class="material-icons">album</i>
 									</button>
 								</p>
 							</div>
 							<div class="duration-container">
 								<label class="label">Duration</label>
-								<p class="control">
+								<p class="control has-addons">
 									<input
 										class="input"
 										type="text"
 										v-model.number="editing.song.duration"
 									/>
+									<button
+										class="button duration-fill-button"
+										v-on:click="fillDuration()"
+									>
+										<i class="material-icons">sync</i>
+									</button>
 								</p>
 							</div>
 							<div class="skip-duration-container">
@@ -126,9 +130,7 @@
 										class="button album-get-button"
 										v-on:click="getAlbumData('albumArt')"
 									>
-										<i class="material-icons album-get-icon"
-											>album</i
-										>
+										<i class="material-icons">album</i>
 									</button>
 								</p>
 							</div>
@@ -150,9 +152,7 @@
 										class="button album-get-button"
 										v-on:click="getAlbumData('artists')"
 									>
-										<i class="material-icons album-get-icon"
-											>album</i
-										>
+										<i class="material-icons">album</i>
 									</button>
 									<button
 										class="button is-info add-button"
@@ -226,9 +226,7 @@
 										class="button album-get-button"
 										v-on:click="getAlbumData('genres')"
 									>
-										<i class="material-icons album-get-icon"
-											>album</i
-										>
+										<i class="material-icons">album</i>
 									</button>
 									<button
 										class="button is-info add-button"
@@ -662,11 +660,6 @@ export default {
 					"Title must have between 1 and 100 characters.",
 					8000
 				);
-			/* if (!validation.regex.ascii.test(song.title))
-				return Toast.methods.addToast(
-					"Invalid title format. Only ascii characters are allowed.",
-					8000
-				); */
 
 			// Artists
 			if (song.artists.length < 1 || song.artists.length > 10)
@@ -676,13 +669,8 @@ export default {
 				);
 			let error;
 			song.artists.forEach(artist => {
-				if (!validation.isLength(artist, 1, 32)) {
-					error = "Artist must have between 1 and 32 characters.";
-					return error;
-				}
-				if (!validation.regex.ascii.test(artist)) {
-					error =
-						"Invalid artist format. Only ascii characters are allowed.";
+				if (!validation.isLength(artist, 1, 64)) {
+					error = "Artist must have between 1 and 64 characters.";
 					return error;
 				}
 				if (artist === "NONE") {
@@ -696,13 +684,13 @@ export default {
 			if (error) return Toast.methods.addToast(error, 8000);
 
 			// Genres
-			/* error = undefined;
+			error = undefined;
 			song.genres.forEach(genre => {
-				if (!validation.isLength(genre, 1, 16)) {
-					error = "Genre must have between 1 and 16 characters.";
+				if (!validation.isLength(genre, 1, 32)) {
+					error = "Genre must have between 1 and 32 characters.";
 					return error;
 				}
-				if (!validation.regex.azAZ09_.test(genre)) {
+				if (!validation.regex.ascii.test(genre)) {
 					error =
 						"Invalid genre format. Only ascii characters are allowed.";
 					return error;
@@ -710,10 +698,12 @@ export default {
 
 				return false;
 			});
-			if (error) return Toast.methods.addToast(error, 8000); */
+			if (song.genres.length < 1 || song.genres.length > 16)
+				error = "You must have between 1 and 16 genres.";
+			if (error) return Toast.methods.addToast(error, 8000);
 
 			// Thumbnail
-			if (!validation.isLength(song.thumbnail, 8, 256))
+			if (!validation.isLength(song.thumbnail, 1, 256))
 				return Toast.methods.addToast(
 					"Thumbnail must have between 8 and 256 characters.",
 					8000
@@ -793,6 +783,10 @@ export default {
 					});
 			}
 		},
+		fillDuration() {
+			this.editing.song.duration =
+				this.youtubeVideoDuration - this.editing.song.skipDuration;
+		},
 		getAlbumData(type) {
 			if (!this.editing.song.discogs) return;
 			if (type === "title")
@@ -1152,8 +1146,8 @@ export default {
 
 		this.discogsQuery = this.editing.song.title;
 
-		lofig.get("cookie.secure", res => {
-			this.useHTTPS = res;
+		lofig.get("cookie.secure").then(useHTTPS => {
+			this.useHTTPS = useHTTPS;
 		});
 
 		io.getSocket(socket => {
@@ -1162,6 +1156,7 @@ export default {
 
 		this.interval = setInterval(() => {
 			if (
+				this.editing.song.duration !== -1 &&
 				this.video.paused === false &&
 				this.playerReady &&
 				this.video.player.getCurrentTime() -
@@ -1220,6 +1215,10 @@ export default {
 						let youtubeDuration = this.video.player.getDuration();
 						this.youtubeVideoDuration = youtubeDuration;
 						this.youtubeVideoNote = "";
+
+						if (this.editing.song.duration === -1)
+							this.editing.song.duration = youtubeDuration;
+
 						youtubeDuration -= this.editing.song.skipDuration;
 						if (this.editing.song.duration > youtubeDuration + 1) {
 							this.video.player.stopVideo();
@@ -1424,6 +1423,14 @@ export default {
 			border-width: 0;
 		}
 
+		.duration-fill-button {
+			background-color: $red;
+			color: $white;
+			width: 32px;
+			text-align: center;
+			border-width: 0;
+		}
+
 		.add-button {
 			background-color: $musareBlue !important;
 			width: 32px;

+ 218 - 52
frontend/components/Modals/EditStation.vue

@@ -39,19 +39,19 @@
 				</span>
 			</p>
 			<br />
-			<p class="control">
+			<p class="control" v-if="station.type === 'community'">
 				<label class="checkbox party-mode-inner">
 					<input v-model="editing.partyMode" type="checkbox" />
 					&nbsp;Party mode
 				</label>
 			</p>
-			<small
+			<small v-if="station.type === 'community'"
 				>With party mode enabled, people can add songs to a queue that
 				plays. With party mode disabled you can play a private playlist
 				on loop.</small
 			>
 			<br />
-			<div v-if="station.partyMode">
+			<div v-if="station.type === 'community' && station.partyMode">
 				<br />
 				<br />
 				<label class="label">Queue lock</label>
@@ -75,6 +75,64 @@
 					Unlock the queue
 				</button>
 			</div>
+			<div
+				v-if="station.type === 'official' && station.genres"
+				class="control is-grouped genre-wrapper"
+			>
+				<div class="sector">
+					<p class="control has-addons">
+						<input
+							id="new-genre-edit"
+							class="input"
+							type="text"
+							placeholder="Genre"
+							@keyup.enter="addGenre()"
+						/>
+						<a class="button is-info" href="#" @click="addGenre()"
+							>Add genre</a
+						>
+					</p>
+					<span
+						v-for="(genre, index) in editing.genres"
+						:key="index"
+						class="tag is-info"
+					>
+						{{ genre }}
+						<button
+							class="delete is-info"
+							@click="removeGenre(index)"
+						/>
+					</span>
+				</div>
+				<div class="sector">
+					<p class="control has-addons">
+						<input
+							id="new-blacklisted-genre-edit"
+							class="input"
+							type="text"
+							placeholder="Blacklisted Genre"
+							@keyup.enter="addBlacklistedGenre()"
+						/>
+						<a
+							class="button is-info"
+							href="#"
+							@click="addBlacklistedGenre()"
+							>Add blacklisted genre</a
+						>
+					</p>
+					<span
+						v-for="(genre, index) in editing.blacklistedGenres"
+						:key="index"
+						class="tag is-info"
+					>
+						{{ genre }}
+						<button
+							class="delete is-info"
+							@click="removeBlacklistedGenre(index)"
+						/>
+					</span>
+				</div>
+			</div>
 		</template>
 		<template v-slot:footer>
 			<button class="button is-success" v-on:click="update()">
@@ -92,17 +150,23 @@
 </template>
 
 <script>
-import { mapState } from "vuex";
-
+import { mapState, mapActions } from "vuex";
 import { Toast } from "vue-roaster";
+
 import Modal from "./Modal.vue";
 import io from "../../io";
 import validation from "../../validation";
 
 export default {
-	computed: mapState("station", {
-		station: state => state.station,
-		editing: state => state.editing
+	computed: mapState({
+		editing(state) {
+			return this.$props.store.split("/").reduce((a, v) => a[v], state)
+				.editing;
+		},
+		station(state) {
+			return this.$props.store.split("/").reduce((a, v) => a[v], state)
+				.station;
+		}
 	}),
 	mounted() {
 		io.getSocket(socket => {
@@ -110,6 +174,7 @@ export default {
 			return socket;
 		});
 	},
+	props: ["store"],
 	methods: {
 		update() {
 			if (this.station.name !== this.editing.name) this.updateName();
@@ -121,6 +186,18 @@ export default {
 				this.updatePrivacy();
 			if (this.station.partyMode !== this.editing.partyMode)
 				this.updatePartyMode();
+			if (this.$props.store !== "station") {
+				if (
+					this.station.genres.toString() !==
+					this.editing.genres.toString()
+				)
+					this.updateGenres();
+				if (
+					this.station.blacklistedGenres.toString() !==
+					this.editing.blacklistedGenres.toString()
+				)
+					this.updateBlacklistedGenres();
+			}
 		},
 		updateName() {
 			const { name } = this.editing;
@@ -141,11 +218,7 @@ export default {
 				name,
 				res => {
 					if (res.status === "success") {
-						if (this.station) {
-							this.station.name = name;
-							return name;
-						}
-
+						if (this.station) this.station.name = name;
 						this.$parent.stations.forEach((station, index) => {
 							if (station._id === this.editing._id) {
 								this.$parent.stations[index].name = name;
@@ -155,8 +228,7 @@ export default {
 							return false;
 						});
 					}
-
-					return Toast.methods.addToast(res.message, 8000);
+					Toast.methods.addToast(res.message, 8000);
 				}
 			);
 		},
@@ -167,9 +239,9 @@ export default {
 					"Display name must have between 2 and 32 characters.",
 					8000
 				);
-			if (!validation.regex.azAZ09_.test(displayName))
+			if (!validation.regex.ascii.test(displayName))
 				return Toast.methods.addToast(
-					"Invalid display name format. Allowed characters: a-z, A-Z, 0-9 and _.",
+					"Invalid display name format. Only ASCII characters are allowed.",
 					8000
 				);
 
@@ -179,19 +251,23 @@ export default {
 				displayName,
 				res => {
 					if (res.status === "success") {
-						if (this.station)
+						if (this.station) {
 							this.station.displayName = displayName;
-						else {
-							this.$parent.stations.forEach((station, index) => {
-								if (station._id === this.editing._id)
-									this.$parent.stations[
-										index
-									].displayName = displayName;
-								return displayName;
-							});
+							return displayName;
 						}
+						this.$parent.stations.forEach((station, index) => {
+							if (station._id === this.editing._id) {
+								this.$parent.stations[
+									index
+								].displayName = displayName;
+								return displayName;
+							}
+
+							return false;
+						});
 					}
-					Toast.methods.addToast(res.message, 8000);
+
+					return Toast.methods.addToast(res.message, 8000);
 				}
 			);
 		},
@@ -202,15 +278,13 @@ export default {
 					"Description must have between 2 and 200 characters.",
 					8000
 				);
-
 			let characters = description.split("");
 			characters = characters.filter(character => {
 				return character.charCodeAt(0) === 21328;
 			});
-
 			if (characters.length !== 0)
 				return Toast.methods.addToast(
-					"Invalid description format. Swastika's are not allowed.",
+					"Invalid description format.",
 					8000
 				);
 
@@ -224,7 +298,6 @@ export default {
 							this.station.description = description;
 							return description;
 						}
-
 						this.$parent.stations.forEach((station, index) => {
 							if (station._id === this.editing._id) {
 								this.$parent.stations[
@@ -244,28 +317,52 @@ export default {
 			);
 		},
 		updatePrivacy() {
-			return this.socket.emit(
+			this.socket.emit(
 				"stations.updatePrivacy",
 				this.editing._id,
 				this.editing.privacy,
 				res => {
 					if (res.status === "success") {
-						if (this.station) {
+						if (this.station)
 							this.station.privacy = this.editing.privacy;
-							return this.editing.privacy;
+						else {
+							this.$parent.stations.forEach((station, index) => {
+								if (station._id === this.editing._id) {
+									this.$parent.stations[
+										index
+									].privacy = this.editing.privacy;
+									return this.editing.privacy;
+								}
+
+								return false;
+							});
 						}
+						return Toast.methods.addToast(res.message, 4000);
+					}
 
+					return Toast.methods.addToast(res.message, 8000);
+				}
+			);
+		},
+		updateGenres() {
+			this.socket.emit(
+				"stations.updateGenres",
+				this.editing._id,
+				this.editing.genres,
+				res => {
+					if (res.status === "success") {
+						const genres = JSON.parse(
+							JSON.stringify(this.editing.genres)
+						);
+						if (this.station) this.station.genres = genres;
 						this.$parent.stations.forEach((station, index) => {
 							if (station._id === this.editing._id) {
-								this.$parent.stations[
-									index
-								].privacy = this.editing.privacy;
-								return this.editing.privacy;
+								this.$parent.stations[index].genres = genres;
+								return genres;
 							}
 
 							return false;
 						});
-
 						return Toast.methods.addToast(res.message, 4000);
 					}
 
@@ -273,28 +370,54 @@ export default {
 				}
 			);
 		},
-		updatePartyMode() {
-			return this.socket.emit(
-				"stations.updatePartyMode",
+		updateBlacklistedGenres() {
+			this.socket.emit(
+				"stations.updateBlacklistedGenres",
 				this.editing._id,
-				this.editing.partyMode,
+				this.editing.blacklistedGenres,
 				res => {
 					if (res.status === "success") {
-						if (this.station) {
-							this.station.partyMode = this.editing.partyMode;
-							return this.editing.partyMode;
-						}
-
+						const blacklistedGenres = JSON.parse(
+							JSON.stringify(this.editing.blacklistedGenres)
+						);
+						if (this.station)
+							this.station.blacklistedGenres = blacklistedGenres;
 						this.$parent.stations.forEach((station, index) => {
 							if (station._id === this.editing._id) {
 								this.$parent.stations[
 									index
-								].partyMode = this.editing.partyMode;
-								return this.editing.partyMode;
+								].blacklistedGenres = blacklistedGenres;
+								return blacklistedGenres;
 							}
 
 							return false;
 						});
+						return Toast.methods.addToast(res.message, 4000);
+					}
+
+					return Toast.methods.addToast(res.message, 8000);
+				}
+			);
+		},
+		updatePartyMode() {
+			this.socket.emit(
+				"stations.updatePartyMode",
+				this.editing._id,
+				this.editing.partyMode,
+				res => {
+					if (res.status === "success") {
+						// if (this.station)
+						// 	this.station.partyMode = this.editing.partyMode;
+						// this.$parent.stations.forEach((station, index) => {
+						// 	if (station._id === this.editing._id) {
+						// 		this.$parent.stations[
+						// 			index
+						// 		].partyMode = this.editing.partyMode;
+						// 		return this.editing.partyMode;
+						// 	}
+
+						// 	return false;
+						// });
 
 						return Toast.methods.addToast(res.message, 4000);
 					}
@@ -303,11 +426,54 @@ export default {
 				}
 			);
 		},
+		addGenre() {
+			const genre = document
+				.getElementById(`new-genre-edit`)
+				.value.toLowerCase()
+				.trim();
+
+			if (this.editing.genres.indexOf(genre) !== -1)
+				return Toast.methods.addToast("Genre already exists", 3000);
+			if (genre) {
+				this.editing.genres.push(genre);
+				document.getElementById(`new-genre-edit`).value = "";
+				return true;
+			}
+			return Toast.methods.addToast("Genre cannot be empty", 3000);
+		},
+		removeGenre(index) {
+			this.editing.genres.splice(index, 1);
+		},
+		addBlacklistedGenre() {
+			const genre = document
+				.getElementById(`new-blacklisted$pa-genre-edit`)
+				.value.toLowerCase()
+				.trim();
+			if (this.editing.blacklistedGenres.indexOf(genre) !== -1)
+				return Toast.methods.addToast("Genre already exists", 3000);
+
+			if (genre) {
+				this.editing.blacklistedGenres.push(genre);
+				document.getElementById(`new-blacklisted-genre-edit`).value =
+					"";
+				return true;
+			}
+			return Toast.methods.addToast("Genre cannot be empty", 3000);
+		},
+		removeBlacklistedGenre(index) {
+			this.editing.blacklistedGenres.splice(index, 1);
+		},
 		deleteStation() {
 			this.socket.emit("stations.remove", this.editing._id, res => {
-				Toast.methods.addToast(res.message, 8000);
+				if (res.status === "success")
+					this.closeModal({
+						sector: "station",
+						modal: "editStation"
+					});
+				return Toast.methods.addToast(res.message, 8000);
 			});
-		}
+		},
+		...mapActions("modals", ["closeModal"])
 	},
 	components: { Modal }
 };

+ 5 - 4
frontend/components/Modals/EditUser.vue

@@ -122,9 +122,9 @@ export default {
 					"Username must have between 2 and 32 characters.",
 					8000
 				);
-			if (!validation.regex.azAZ09_.test(username))
+			if (!validation.regex.custom("a-zA-Z0-9_-").test(username))
 				return Toast.methods.addToast(
-					"Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _.",
+					"Invalid username format. Allowed characters: a-z, A-Z, 0-9, _ and -.",
 					8000
 				);
 
@@ -138,7 +138,7 @@ export default {
 			);
 		},
 		updateEmail() {
-			const { email } = this.editing;
+			const email = this.editing.email.address;
 			if (!validation.isLength(email, 3, 254))
 				return Toast.methods.addToast(
 					"Email must have between 3 and 254 characters.",
@@ -146,7 +146,8 @@ export default {
 				);
 			if (
 				email.indexOf("@") !== email.lastIndexOf("@") ||
-				!validation.regex.emailSimple.test(email)
+				!validation.regex.emailSimple.test(email) ||
+				!validation.regex.ascii.test(email)
 			)
 				return Toast.methods.addToast("Invalid email format.", 8000);
 

+ 2 - 2
frontend/components/Modals/Login.vue

@@ -114,8 +114,8 @@ export default {
 		...mapActions("user/auth", ["login"])
 	},
 	mounted() {
-		lofig.get("serverDomain", res => {
-			this.serverDomain = res;
+		lofig.get("serverDomain").then(serverDomain => {
+			this.serverDomain = serverDomain;
 		});
 	}
 };

+ 2 - 2
frontend/components/Modals/Playlists/Create.vue

@@ -51,9 +51,9 @@ export default {
 					"Display name must have between 2 and 32 characters.",
 					8000
 				);
-			if (!validation.regex.azAZ09_.test(displayName))
+			if (!validation.regex.ascii.test(displayName))
 				return Toast.methods.addToast(
-					"Invalid display name format. Allowed characters: a-z, A-Z, 0-9 and _.",
+					"Invalid display name format. Only ASCII characters are allowed.",
 					8000
 				);
 

+ 2 - 2
frontend/components/Modals/Playlists/Edit.vue

@@ -322,9 +322,9 @@ export default {
 					"Display name must have between 2 and 32 characters.",
 					8000
 				);
-			if (!validation.regex.azAZ09_.test(displayName))
+			if (!validation.regex.ascii.test(displayName))
 				return Toast.methods.addToast(
-					"Invalid display name format. Allowed characters: a-z, A-Z, 0-9 and _.",
+					"Invalid display name format. Only ASCII characters are allowed.",
 					8000
 				);
 

+ 3 - 3
frontend/components/Modals/Register.vue

@@ -100,11 +100,11 @@ export default {
 		};
 	},
 	mounted() {
-		lofig.get("serverDomain", res => {
-			this.serverDomain = res;
+		lofig.get("serverDomain").then(serverDomain => {
+			this.serverDomain = serverDomain;
 		});
 
-		lofig.get("recaptcha", obj => {
+		lofig.get("recaptcha").then(obj => {
 			this.recaptcha.key = obj.key;
 
 			const recaptchaScript = document.createElement("script");

+ 1 - 1
frontend/components/Station/Station.vue

@@ -12,7 +12,7 @@
 		<add-to-playlist v-if="modals.addSongToPlaylist" />
 		<edit-playlist v-if="modals.editPlaylist" />
 		<create-playlist v-if="modals.createPlaylist" />
-		<edit-station v-show="modals.editStation" />
+		<edit-station v-show="modals.editStation" store="station" />
 		<report v-if="modals.report" />
 
 		<transition name="slide">

+ 5 - 6
frontend/components/Station/StationHeader.vue

@@ -236,13 +236,12 @@ export default {
 		currentSong: state => state.station.currentSong
 	}),
 	mounted() {
-		lofig.get("frontendDomain", res => {
-			this.frontendDomain = res;
-			return res;
+		lofig.get("frontendDomain").then(frontendDomain => {
+			this.frontendDomain = frontendDomain;
 		});
-		lofig.get("siteSettings", res => {
-			this.siteSettings = res;
-			return res;
+
+		lofig.get("siteSettings").then(siteSettings => {
+			this.siteSettings = siteSettings;
 		});
 	},
 	methods: {

+ 2 - 2
frontend/components/User/Settings.vue

@@ -175,8 +175,8 @@ export default {
 		userId: state => state.user.auth.userId
 	}),
 	mounted() {
-		lofig.get("serverDomain", res => {
-			this.serverDomain = res;
+		lofig.get("serverDomain").then(serverDomain => {
+			this.serverDomain = serverDomain;
 		});
 
 		io.getSocket(socket => {

+ 1 - 1
frontend/dist/index.tpl.html

@@ -39,7 +39,7 @@
 	<script src='https://www.youtube.com/iframe_api'></script>
 	<script type='text/javascript' src='/vendor/can-autoplay.min.js'></script>
 	<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.js" integrity="sha256-yr4fRk/GU1ehYJPAs8P4JlTgu0Hdsp4ZKrx8bDEDC3I=" crossorigin="anonymous"></script>
-	<script type='text/javascript' src='/lofig.min.js'></script>
+	<script type='text/javascript' src='https://unpkg.com/lofig@1.2.1/dist/lofig.min.js'></script>
 </head>
 <body>
 	<div id="root"></div>

File diff suppressed because it is too large
+ 0 - 0
frontend/dist/lofig.min.js


+ 2 - 2
frontend/main.js

@@ -125,8 +125,8 @@ const router = new VueRouter({
 });
 
 lofig.folder = "../config/default.json";
-lofig.get("serverDomain", res => {
-	io.init(res);
+lofig.get("serverDomain").then(serverDomain => {
+	io.init(serverDomain);
 	io.getSocket(socket => {
 		socket.on("ready", (loggedIn, role, username, userId) => {
 			store.dispatch("user/auth/authData", {

+ 4 - 2
frontend/validation.js

@@ -3,8 +3,10 @@ module.exports = {
 		azAZ09_: /^[A-Za-z0-9_]+$/,
 		az09_: /^[a-z0-9_]+$/,
 		emailSimple: /^[\x00-\x7F]+@[a-z0-9]+\.[a-z0-9]+(\.[a-z0-9]+)?$/,
-		password: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!%*?&])[A-Za-z\d$@$!%*?&]/,
-		ascii: /^[\x00-\x7F]+$/
+		ascii: /^[\x00-\x7F]+$/,
+		custom: regex => {
+			return new RegExp(`^[${regex}]+$`);
+		}
 	},
 	isLength: (string, min, max) => {
 		return !(

Some files were not shown because too many files changed in this diff