Răsfoiți Sursa

Merge pull request #31 from Musare/staging

Updated Backup
Jonathan 8 ani în urmă
părinte
comite
b693e6d458

+ 1 - 5
.gitignore

@@ -22,8 +22,4 @@ frontend/build/config/default.json
 npm
 
 # Logs
-all.log
-error.log
-info.log
-success.log
-
+log/

+ 12 - 4
README.md

@@ -99,7 +99,7 @@ Now you have different paths here.
 
 ####Non-docker
 
-Steps 1-4 are things you only have to do once. The steps after that are steps you want to do when you want to start the site.
+Steps 1-4 are things you only have to do once. The steps to start servers follow.
 
 1. In the main folder, create a folder called `.database`
 
@@ -117,11 +117,19 @@ Steps 1-4 are things you only have to do once. The steps after that are steps yo
 
 	And again, make sure that the paths lead to the proper config and executable.
 
-5. Run `startRedis.cmd` and `startMongo.cmd` to start Redis and Mongo.
+####Non-docker start servers
 
-6. In a command prompt with the pwd of frontend, run `npm run development-watch`
+**Automatic**
 
-7. In a command prompt with the pwd of backend, run `nodemon`
+1.  If you are on Windows you can run `windows-start.cmd` or just double click the `windows-start.cmd` file and all servers will automatically start up.
+
+**Manual**
+
+1. Run `startRedis.cmd` and `startMongo.cmd` to start Redis and Mongo.
+
+2. In a command prompt with the pwd of frontend, run `npm run development-watch`
+
+3. In a command prompt with the pwd of backend, run `nodemon`
 
 ## Extra
 

+ 70 - 9
backend/logic/actions/stations.js

@@ -416,6 +416,13 @@ module.exports = {
 
 			(station, next) => {
 				if (!station) return next('Station not found.');
+				utils.canUserBeInStation(station, userId, (canBe) => {
+					if (canBe) return next(null, station);
+					return next('Insufficient permissions.');
+				});
+			},
+
+			(station, next) => {
 				if (!station.currentSong) return next('There is currently no song to skip.');
 				if (station.currentSong.skipVotes.indexOf(userId) !== -1) return next('You have already voted to skip this song.');
 				next(null, station);
@@ -493,10 +500,6 @@ module.exports = {
 			(station, next) => {
 				if (!station) return next('Station not found.');
 				next();
-			},
-
-			(next) => {
-				cache.client.hincrby('station.userCounts', stationId, -1, next);
 			}
 		], (err, userCount) => {
 			if (err) {
@@ -842,6 +845,13 @@ module.exports = {
 			(station, next) => {
 				if (!station) return next('Station not found.');
 				if (station.type !== 'community') return next('That station is not a community station.');
+				utils.canUserBeInStation(station, userId, (canBe) => {
+					if (canBe) return next(null, station);
+					return next('Insufficient permissions.');
+				});
+			},
+
+			(station, next) => {
 				if (station.currentSong && station.currentSong.songId === songId) return next('That song is currently playing.');
 				async.each(station.queue, (queueSong, next) => {
 					if (queueSong.songId === songId) return next('That song is already in the queue.');
@@ -853,7 +863,7 @@ module.exports = {
 
 			(station, next) => {
 				songs.getSong(songId, (err, song) => {
-					if (!err && song) return next(null, song);
+					if (!err && song) return next(null, song, station);
 					utils.getSongFromYouTube(songId, (song) => {
 						song.artists = [];
 						song.skipDuration = 0;
@@ -861,13 +871,57 @@ module.exports = {
 						song.dislikes = -1;
 						song.thumbnail = "empty";
 						song.explicit = false;
-						next(null, song);
+						next(null, song, station);
 					});
 				});
 			},
 
-			(song, next) => {
+			(song, station, next) => {
+				let queue = station.queue;
 				song.requestedBy = userId;
+				queue.push(song);
+
+				let totalDuration = 0;
+				queue.forEach((song) => {
+					totalDuration += song.duration;
+				});
+				if (totalDuration >= 3600 * 3) return next('The max length of the queue is 3 hours.');
+				next(null, song, station);
+			},
+
+			(song, station, next) => {
+				let queue = station.queue;
+				if (queue.length === 0) return next(null, song, station);
+				let totalDuration = 0;
+				const userId = queue[queue.length - 1].requestedBy;
+				station.queue.forEach((song) => {
+					if (userId === song.requestedBy) {
+						totalDuration += song.duration;
+					}
+				});
+
+				if(totalDuration >= 900) return next('The max length of songs per user is 15 minutes.');
+				next(null, song, station);
+			},
+
+			(song, station, next) => {
+				let queue = station.queue;
+				if (queue.length === 0) return next(null, song);
+				let totalSongs = 0;
+				const userId = queue[queue.length - 1].requestedBy;
+				queue.forEach((song) => {
+					if (userId === song.requestedBy) {
+						totalSongs++;
+					}
+				});
+
+				if (totalSongs <= 2) return next(null, song);
+				if (totalSongs > 3) return next('The max amount of songs per user is 3, and only 2 in a row is allowed.');
+				if (queue[queue.length - 2].requestedBy !== userId || queue[queue.length - 3] !== userId) return next('The max amount of songs per user is 3, and only 2 in a row is allowed.');
+				next(null, song);
+			},
+
+			(song, next) => {
 				db.models.station.update({_id: stationId}, {$push: {queue: song}}, {runValidators: true}, next);
 			},
 
@@ -940,7 +994,7 @@ module.exports = {
 	 * @param stationId - the station id
 	 * @param cb
 	 */
-	getQueue: hooks.adminRequired((session, stationId, cb) => {
+	getQueue: (session, stationId, cb) => {
 		async.waterfall([
 			(next) => {
 				stations.getStation(stationId, next);
@@ -950,6 +1004,13 @@ module.exports = {
 				if (!station) return next('Station not found.');
 				if (station.type !== 'community') return next('Station is not a community station.');
 				next(null, station);
+			},
+
+			(station, next) => {
+				utils.canUserBeInStation(station, session.userId, (canBe) => {
+					if (canBe) return next(null, station);
+					return next('Insufficient permissions.');
+				});
 			}
 		], (err, station) => {
 			if (err) {
@@ -960,7 +1021,7 @@ module.exports = {
 			logger.success("STATIONS_GET_QUEUE", `Got queue for station "${stationId}" successfully.`);
 			return cb({'status': 'success', 'message': 'Successfully got queue.', queue: station.queue});
 		});
-	}),
+	},
 
 	/**
 	 * Selects a private playlist for a station

+ 3 - 1
backend/logic/db/index.js

@@ -88,12 +88,13 @@ let lib = {
 				});
 			}, 'User already has 3 stations.');
 
+			/*
 			lib.schemas.station.path('queue').validate((queue, callback) => {
 				let totalDuration = 0;
 				queue.forEach((song) => {
 					totalDuration += song.duration;
 				});
-				return callback(totalDuration <= 3600);
+				return callback(totalDuration <= 3600 * 3);
 			}, 'The max length of the queue is 3 hours.');
 
 			lib.schemas.station.path('queue').validate((queue, callback) => {
@@ -122,6 +123,7 @@ let lib = {
 				if (queue[queue.length - 2].requestedBy !== userId || queue[queue.length - 3] !== userId) return callback(true);
 				return callback(false);
 			}, 'The max amount of songs per user is 3, and only 2 in a row is allowed.');
+			*/
 
 			let songTitle = (title) => {
 				return (isLength(title, 1, 64) && regex.ascii.test(title));

+ 12 - 10
backend/logic/logger.js

@@ -1,11 +1,13 @@
 'use strict';
 
+const dir = `${__dirname}/../../log`;
 const fs = require('fs');
+const config = require('config');
 let utils;
-/*const log_file = fs.createWriteStream(__dirname + '/../../all.log', {flags : 'w'});
-const success_log_file = fs.createWriteStream(__dirname + '/../../success.log', {flags : 'w'});
-const error_log_file = fs.createWriteStream(__dirname + '/../../error.log', {flags : 'w'});
-const info_log_file = fs.createWriteStream(__dirname + '/../../info.log', {flags : 'w'});*/
+
+if (!config.isDocker && !fs.existsSync(`${dir}`)) {
+	fs.mkdirSync(dir);
+}
 
 let started;
 let success = 0;
@@ -97,8 +99,8 @@ module.exports = {
 		successThisHour++;
 		getTime((time) => {
 			let timeString = `${time.year}-${twoDigits(time.month)}-${twoDigits(time.day)} ${twoDigits(time.hour)}:${twoDigits(time.minute)}:${twoDigits(time.second)}`;
-			fs.appendFile(__dirname + '/../../all.log', `${timeString} SUCCESS - ${type} - ${message}\n`, ()=>{});
-			fs.appendFile(__dirname + '/../../success.log', `${timeString} SUCCESS - ${type} - ${message}\n`, ()=>{});
+			fs.appendFile(dir + '/all.log', `${timeString} SUCCESS - ${type} - ${message}\n`, ()=>{});
+			fs.appendFile(dir + '/success.log', `${timeString} SUCCESS - ${type} - ${message}\n`, ()=>{});
 			console.info('\x1b[32m', timeString, 'SUCCESS', '-', type, '-', message, '\x1b[0m');
 		});
 	},
@@ -108,8 +110,8 @@ module.exports = {
 		errorThisHour++;
 		getTime((time) => {
 			let timeString = `${time.year}-${twoDigits(time.month)}-${twoDigits(time.day)} ${twoDigits(time.hour)}:${twoDigits(time.minute)}:${twoDigits(time.second)}`;
-			fs.appendFile(__dirname + '/../../all.log', `${timeString} ERROR - ${type} - ${message}\n`, ()=>{});
-			fs.appendFile(__dirname + '/../../error.log', `${timeString} ERROR - ${type} - ${message}\n`, ()=>{});
+			fs.appendFile(dir + '/all.log', `${timeString} ERROR - ${type} - ${message}\n`, ()=>{});
+			fs.appendFile(dir + '/error.log', `${timeString} ERROR - ${type} - ${message}\n`, ()=>{});
 			console.warn('\x1b[31m', timeString, 'ERROR', '-', type, '-', message, '\x1b[0m');
 		});
 	},
@@ -119,8 +121,8 @@ module.exports = {
 		infoThisHour++;
 		getTime((time) => {
 			let timeString = `${time.year}-${twoDigits(time.month)}-${twoDigits(time.day)} ${twoDigits(time.hour)}:${twoDigits(time.minute)}:${twoDigits(time.second)}`;
-			fs.appendFile(__dirname + '/../../all.log', `${timeString} INFO - ${type} - ${message}\n`, ()=>{});
-			fs.appendFile(__dirname + '/../../info.log', `${timeString} INFO - ${type} - ${message}\n`, ()=>{});
+			fs.appendFile(dir + '/all.log', `${timeString} INFO - ${type} - ${message}\n`, ()=>{});
+			fs.appendFile(dir + '/info.log', `${timeString} INFO - ${type} - ${message}\n`, ()=>{});
 
 			console.info('\x1b[36m', timeString, 'INFO', '-', type, '-', message, '\x1b[0m');
 		});

+ 1 - 1
backend/logic/tasks.js

@@ -29,7 +29,7 @@ let checkStationSkipTask = (callback) => {
 				if (timeElapsed <= station.currentSong.duration) return next2();
 				else {
 					logger.error("TASK_STATIONS_SKIP_CHECK", `Skipping ${station._id} as it should have skipped already.`);
-					stations.skipStation(station._id);
+					Stations.skipStation(station._id);
 					next2();
 				}
 			}, () => {

+ 29 - 1
backend/logic/utils.js

@@ -2,6 +2,7 @@
 
 const moment  = require('moment'),
 	  io      = require('./io'),
+	  db      = require('./db'),
 	  config  = require('config'),
 	  async	  = require('async'),
 	  request = require('request'),
@@ -422,7 +423,34 @@ module.exports = {
 	getError: (err) => {
 		let error = 'An error occurred.';
 		if (typeof err === "string") error = err;
-		else if (err.message) error = err.message;
+		else if (err.message) {
+			if (err.message !== 'Validation failed') error = err.message;
+			else error = err.errors[Object.keys(err.errors)].message;
+		}
 		return error;
+	},
+	canUserBeInStation: (station, userId, cb) => {
+		async.waterfall([
+			(next) => {
+				if (station.privacy !== 'private') return next(true);
+				if (!userId) return next(false);
+				next();
+			},
+
+			(next) => {
+				db.models.user.findOne({_id: userId}, next);
+			},
+
+			(user, next) => {
+				if (!user) return next(false);
+				if (user.role === 'admin') return next(true);
+				if (station.type === 'official') return next(false);
+				if (station.owner === userId) return next(true);
+				next(false);
+			}
+		], (err) => {
+			if (err === true) return cb(true);
+			return cb(false);
+		});
 	}
 };

+ 1 - 1
backend/package.json

@@ -22,7 +22,7 @@
     "express-session": "^1.14.0",
     "mailgun-js": "^0.8.0",
     "moment": "^2.15.2",
-    "mongoose": "^4.6.0",
+    "mongoose": "^4.9.0",
     "oauth": "^0.9.14",
     "passport": "^0.3.2",
     "passport-discord": "^0.1.1",

+ 1 - 0
docker-compose.yml

@@ -6,6 +6,7 @@ services:
     - "8080:8080"
     volumes:
     - ./backend:/opt/app
+    - ./log:/opt/log
     links:
     - mongo
     - redis

+ 17 - 2
frontend/App.vue

@@ -17,6 +17,7 @@
 	import RegisterModal from './components/Modals/Register.vue';
 	import auth from './auth';
 	import io from './io';
+	import validation from './validation';
 
 	export default {
 		replace: false,
@@ -84,10 +85,24 @@
 			}
 		},
 		events: {
-			'register': function () {
+			'register': function (recaptchaId) {
 				let { register: { email, username, password } } = this;
 				let _this = this;
-				this.socket.emit('users.register', username, email, password, grecaptcha.getResponse(), result => {
+				if (!email || !username || !password) return Toast.methods.addToast('Please fill in all fields', 8000);
+
+
+				if (!validation.isLength(email, 3, 254)) return Toast.methods.addToast('Email must have between 3 and 254 characters.', 8000);
+				if (email.indexOf('@') !== email.lastIndexOf('@') || !validation.regex.emailSimple.test(email)) return Toast.methods.addToast('Invalid email format.', 8000);
+
+
+				if (!validation.isLength(username, 2, 32)) return Toast.methods.addToast('Username must have between 2 and 32 characters.', 8000);
+				if (!validation.regex.azAZ09_.test(username)) return Toast.methods.addToast('Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _.', 8000);
+
+
+				if (!validation.isLength(password, 6, 200)) return Toast.methods.addToast('Password must have between 6 and 200 characters.', 8000);
+				if (!validation.regex.password.test(password)) return Toast.methods.addToast('Invalid password format. Must have one lowercase letter, one uppercase letter, one number and one special character.', 8000);
+
+				this.socket.emit('users.register', username, email, password, grecaptcha.getResponse(recaptchaId), result => {
 					if (result.status === 'success') {
 						Toast.methods.addToast(`You have successfully registered.`, 4000);
 						if (result.SID) {

+ 8 - 0
frontend/build/index.html

@@ -42,6 +42,14 @@
 	<script type='text/javascript' src='https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.15.0/moment.min.js'></script>
 	<script type='text/javascript' src='https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.4.8/socket.io.min.js'></script>
 	<script type='text/javascript' src='https://cdn.rawgit.com/atjonathan/lofig/master/dist/lofig.min.js'></script>
+	<script>
+		(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+					(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+				m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+		})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
+
+		ga('create', 'UA-93460758-1', 'auto');
+	</script>
 </head>
 <body>
 	<script src='https://www.google.com/recaptcha/api.js'></script>

+ 56 - 24
frontend/components/Modals/AddSongToPlaylist.vue

@@ -1,17 +1,19 @@
 <template>
 	<modal title='Add Song To Playlist'>
 		<div slot='body'>
+			<h4 class="songTitle">{{ $parent.currentSong.title }}</h4>
+			<h5 class="songArtist">{{ $parent.currentSong.artists }}</h5>
 			<aside class="menu">
 				<p class="menu-label">
 					Playlists
 				</p>
 				<ul class="menu-list">
-					<li v-for='playlist in playlists'>
+					<li v-for='playlist in playlistsArr'>
 						<div class='playlist'>
-							<span class='icon is-small' @click='removeSongFromPlaylist(playlist._id)' v-if='playlistContains(playlist._id)'>
+							<span class='icon is-small' @click='removeSongFromPlaylist(playlist._id)' v-if='playlists[playlist._id].hasSong'>
 								<i class="material-icons">playlist_add_check</i>
 							</span>
-							<span class='icon is-small' @click='addSongToPlaylist(playlist._id)' v-else>
+							<span class='icon' @click='addSongToPlaylist(playlist._id)' v-else>
 								<i class="material-icons">playlist_add</i>
 							</span>
 							{{ playlist.displayName }}
@@ -32,47 +34,65 @@
 	export default {
 		data() {
 			return {
-				playlists: {}
+				playlists: {},
+				playlistsArr: [],
+				songId: null,
+				song: null
 			}
 		},
 		methods: {
-			playlistContains: function (playlistId) {
-				let _this = this;
-				let toReturn = false;
-
-				let playlist = this.playlists.filter(playlist => {
-				    return playlist._id === playlistId;
-				})[0];
-
-				for (let i = 0; i < playlist.songs.length; i++) {
-					if (playlist.songs[i].songId === _this.$parent.currentSong.songId) {
-						toReturn = true;
-					}
-				}
-
-				return toReturn;
-			},
 			addSongToPlaylist: function (playlistId) {
 				let _this = this;
 				this.socket.emit('playlists.addSongToPlaylist', this.$parent.currentSong.songId, playlistId, res => {
 					Toast.methods.addToast(res.message, 4000);
-					this.$parent.modals.addSongToPlaylist = false;
+					if (res.status === 'success') {
+						_this.playlists[playlistId].songs.push(_this.song);
+					}
+					_this.recalculatePlaylists();
+					//this.$parent.modals.addSongToPlaylist = false;
 				});
 			},
 			removeSongFromPlaylist: function (playlistId) {
 				let _this = this;
-				this.socket.emit('playlists.removeSongFromPlaylist', this.$parent.currentSong.songId, playlistId, res => {
+				this.socket.emit('playlists.removeSongFromPlaylist', _this.songId, playlistId, res => {
 					Toast.methods.addToast(res.message, 4000);
-					this.$parent.modals.addSongToPlaylist = false;
+					if (res.status === 'success') {
+						_this.playlists[playlistId].songs.forEach((song, index) => {
+							if (song.songId === _this.songId) _this.playlists[playlistId].songs.splice(index, 1);
+						});
+					}
+					_this.recalculatePlaylists();
+					//this.$parent.modals.addSongToPlaylist = false;
+				});
+			},
+			recalculatePlaylists: function() {
+				let _this = this;
+				_this.playlistsArr = Object.values(_this.playlists).map((playlist) => {
+					let hasSong = false;
+					for (let i = 0; i < playlist.songs.length; i++) {
+						if (playlist.songs[i].songId === _this.songId) {
+							hasSong = true;
+						}
+					}
+					playlist.hasSong = hasSong;
+					_this.playlists[playlist._id] = playlist;
+					return playlist;
 				});
 			}
 		},
 		ready: function () {
 			let _this = this;
+			this.songId = this.$parent.currentSong.songId;
+			this.song = this.$parent.currentSong;
 			io.getSocket((socket) => {
 				_this.socket = socket;
 				_this.socket.emit('playlists.indexForUser', res => {
-					if (res.status === 'success') _this.playlists = res.data;
+					if (res.status === 'success') {
+						res.data.forEach((playlist) => {
+							_this.playlists[playlist._id] = playlist;
+						});
+						_this.recalculatePlaylists();
+					}
 				});
 			});
 		},
@@ -89,4 +109,16 @@
 	.icon.is-small {
 		margin-right: 10px !important;
 	}
+	.songTitle {
+		font-size: 22px;
+		padding: 0 10px;
+	}
+	.songArtist {
+		font-size: 19px;
+		font-weight: 200;
+		padding: 0 10px;
+	}
+	.menu-label {
+		font-size: 16px;
+	}
 </style>

+ 26 - 8
frontend/components/Modals/CreateCommunityStation.vue

@@ -2,7 +2,7 @@
 	<modal title='Create Community Station'>
 		<div slot='body'>
 			<!-- validation to check if exists http://bulma.io/documentation/elements/form/ -->
-			<label class='label'>Name (lowercase, a-z, used in the url)</label>
+			<label class='label'>Name (unique lowercase station id)</label>
 			<p class='control'>
 				<input class='input' type='text' placeholder='Name...' v-model='newCommunity.name' autofocus>
 			</p>
@@ -25,6 +25,7 @@
 	import { Toast } from 'vue-roaster';
 	import Modal from './Modal.vue';
 	import io from '../../io';
+	import validation from '../../validation';
 
 	export default {
 		components: { Modal },
@@ -48,15 +49,32 @@
 				this.$parent.modals.createCommunityStation = !this.$parent.modals.createCommunityStation;
 			},
 			submitModal: function () {
-				let _this = this;
-				if (_this.newCommunity.name == '') return Toast.methods.addToast('Name cannot be a blank field', 3000);
-				if (_this.newCommunity.displayName == '') return Toast.methods.addToast('Display Name cannot be a blank field', 3000);
-				if (_this.newCommunity.description == '') return Toast.methods.addToast('Description cannot be a blank field', 3000);
+				const name = this.newCommunity.name;
+				const displayName = this.newCommunity.displayName;
+				const description = this.newCommunity.description;
+				if (!name || !displayName || !description) return Toast.methods.addToast('Please fill in all fields', 8000);
+
+				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);
+
+
+				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);
+
+
+				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(function(character) {
+					return character.charCodeAt(0) === 21328;
+				});
+				if (characters.length !== 0) return Toast.methods.addToast('Invalid description format. Swastika\'s are not allowed.', 8000);
+
+
 				this.socket.emit('stations.create', {
-					name: _this.newCommunity.name,
+					name: name,
 					type: 'community',
-					displayName: _this.newCommunity.displayName,
-					description: _this.newCommunity.description
+					displayName: displayName,
+					description: description
 				}, res => {
 					if (res.status === 'success') Toast.methods.addToast(`You have added the station successfully`, 4000);
 					else Toast.methods.addToast(res.message, 4000);

+ 65 - 63
frontend/components/Modals/EditStation.vue

@@ -3,58 +3,36 @@
 		<modal title='Edit Station'>
 			<div slot='body'>
 				<label class='label'>Name</label>
-				<div class='control is-grouped'>
-					<p class='control is-expanded'>
-						<input class='input' type='text' placeholder='Station Name' v-model='editing.name'>
-					</p>
-					<p class='control'>
-						<a class='button is-info' @click='updateName()' href='#'>Update</a>
-					</p>
-				</div>
+				<p class='control'>
+					<input class='input' type='text' placeholder='Station Name' v-model='editing.name'>
+				</p>
 				<label class='label'>Display name</label>
-				<div class='control is-grouped'>
-					<p class='control is-expanded'>
-						<input class='input' type='text' placeholder='Station Display Name' v-model='editing.displayName'>
-					</p>
-					<p class='control'>
-						<a class='button is-info' @click='updateDisplayName()' href='#'>Update</a>
-					</p>
-				</div>
+				<p class='control'>
+					<input class='input' type='text' placeholder='Station Display Name' v-model='editing.displayName'>
+				</p>
 				<label class='label'>Description</label>
-				<div class='control is-grouped'>
-					<p class='control is-expanded'>
-						<input class='input' type='text' placeholder='Station Display Name' v-model='editing.description'>
-					</p>
-					<p class='control'>
-						<a class='button is-info' @click='updateDescription()' href='#'>Update</a>
-					</p>
-				</div>
+				<p class='control'>
+					<input class='input' type='text' placeholder='Station Display Name' v-model='editing.description'>
+				</p>
 				<label class='label'>Privacy</label>
-				<div class='control is-grouped'>
-					<p class='control is-expanded'>
-							<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>
-					<p class='control'>
-						<a class='button is-info' @click='updatePrivacy()' href='#'>Update</a>
-					</p>
-				</div>
-				<div class='control is-grouped' v-if="editing.type === 'community'">
-					<p class="control is-expanded party-mode-outer">
-						<label class="checkbox party-mode-inner">
-							<input type="checkbox" v-model="editing.partyMode">
-							&nbsp;Party mode
-						</label>
-					</p>
-					<p class='control'>
-						<a class='button is-info' @click='updatePartyMode()' href='#'>Update</a>
-					</p>
-				</div>
+				<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>
+				<p class='control'>
+					<label class="checkbox party-mode-inner">
+						<input type="checkbox" v-model="editing.partyMode">
+						&nbsp;Party mode
+					</label>
+				</p>
+			</div>
+			<div slot='footer'>
+				<button class='button is-success' @click='update()'>Update Settings</button>
 				<button class='button is-danger' @click='deleteStation()' v-if="$parent.type === 'community'">Delete station</button>
 			</div>
 		</modal>
@@ -65,6 +43,7 @@
 	import { Toast } from 'vue-roaster';
 	import Modal from './Modal.vue';
 	import io from '../../io';
+	import validation from '../../validation';
 
 	export default {
 		data: function() {
@@ -81,14 +60,25 @@
 			}
 		},
 		methods: {
+			update: function () {
+				if (this.$parent.station.name !== this.editing.name) this.updateName();
+				if (this.$parent.station.displayName !== this.editing.displayName) this.updateDisplayName();
+				if (this.$parent.station.description !== this.editing.description) this.updateDescription();
+				if (this.$parent.station.privacy !== this.editing.privacy) this.updatePrivacy();
+				if (this.$parent.station.partyMode !== this.editing.partyMode) this.updatePartyMode();
+			},
 			updateName: function () {
-				let _this = this;
-				this.socket.emit('stations.updateName', this.editing._id, this.editing.name, res => {
+				const name = this.editing.name;
+				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);
+
+
+				this.socket.emit('stations.updateName', this.editing._id, name, res => {
 					if (res.status === 'success') {
-						if (_this.$parent.station) _this.$parent.station.name = _this.editing.name;
+						if (this.$parent.station) _this.$parent.station.name = name;
 						else {
-							_this.$parent.stations.forEach((station, index) => {
-								if (station._id === _this.editing._id) return _this.$parent.stations[index].name = _this.editing.name;
+							this.$parent.stations.forEach((station, index) => {
+								if (station._id === this.editing._id) return this.$parent.stations[index].name = name;
 							});
 						}
 					}
@@ -96,13 +86,17 @@
 				});
 			},
 			updateDisplayName: function () {
-				let _this = this;
-				this.socket.emit('stations.updateDisplayName', this.editing._id, this.editing.displayName, res => {
+				const displayName = this.editing.displayName;
+				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);
+
+
+				this.socket.emit('stations.updateDisplayName', this.editing._id, displayName, res => {
 					if (res.status === 'success') {
-						if (_this.$parent.station) _this.$parent.station.displayName = _this.editing.displayName;
+						if (this.$parent.station) _this.$parent.station.displayName = displayName;
 						else {
-							_this.$parent.stations.forEach((station, index) => {
-								if (station._id === _this.editing._id) return _this.$parent.stations[index].displayName = _this.editing.displayName;
+							this.$parent.stations.forEach((station, index) => {
+								if (station._id === this.editing._id) return this.$parent.stations[index].displayName = displayName;
 							});
 						}
 					}
@@ -110,13 +104,21 @@
 				});
 			},
 			updateDescription: function () {
-				let _this = this;
-				this.socket.emit('stations.updateDescription', this.editing._id, this.editing.description, res => {
+				const description = this.editing.description;
+				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(function(character) {
+					return character.charCodeAt(0) === 21328;
+				});
+				if (characters.length !== 0) return Toast.methods.addToast('Invalid description format. Swastika\'s are not allowed.', 8000);
+
+
+				this.socket.emit('stations.updateDescription', this.editing._id, description, res => {
 					if (res.status === 'success') {
-						if (_this.$parent.station) _this.$parent.station.description = _this.editing.description;
+						if (_this.$parent.station) _this.$parent.station.description = description;
 						else {
 							_this.$parent.stations.forEach((station, index) => {
-								if (station._id === station._id) return _this.$parent.stations[index].description = _this.editing.description;
+								if (station._id === station._id) return _this.$parent.stations[index].description = description;
 							});
 						}
 						return Toast.methods.addToast(res.message, 4000);

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

@@ -39,6 +39,7 @@
 	import io from '../../io';
 	import { Toast } from 'vue-roaster';
 	import Modal from './Modal.vue';
+	import validation from '../../validation';
 
 	export default {
 		components: { Modal },
@@ -49,23 +50,32 @@
 		},
 		methods: {
 			updateUsername: function () {
-				this.socket.emit(`users.updateUsername`, this.editing._id, this.editing.username, res => {
+				const username = this.editing.username;
+				if (!validation.isLength(username, 2, 32)) return Toast.methods.addToast('Username must have between 2 and 32 characters.', 8000);
+				if (!validation.regex.azAZ09_.test(username)) return Toast.methods.addToast('Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _.', 8000);
+
+
+				this.socket.emit(`users.updateUsername`, this.editing._id, username, res => {
 					Toast.methods.addToast(res.message, 4000);
 				});
 			},
 			updateEmail: function () {
-				this.socket.emit(`users.updateEmail`, this.editing._id, this.editing.email, res => {
+				const email = this.editing.email;
+				if (!validation.isLength(email, 3, 254)) return Toast.methods.addToast('Email must have between 3 and 254 characters.', 8000);
+				if (email.indexOf('@') !== email.lastIndexOf('@') || !validation.regex.emailSimple.test(email)) return Toast.methods.addToast('Invalid email format.', 8000);
+
+
+				this.socket.emit(`users.updateEmail`, this.editing._id, email, res => {
 					Toast.methods.addToast(res.message, 4000);
 				});
 			},
 			updateRole: function () {
-				let _this = this;
 				this.socket.emit(`users.updateRole`, this.editing._id, this.editing.role, res => {
 					Toast.methods.addToast(res.message, 4000);
 					if (
 							res.status === 'success' &&
-							_this.editing.role === 'default' &&
-							_this.editing._id === _this.$parent.$parent.$parent.userId
+							this.editing.role === 'default' &&
+							this.editing._id === this.$parent.$parent.$parent.userId
 					) location.reload();
 				});
 			}

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

@@ -15,6 +15,7 @@
 	import { Toast } from 'vue-roaster';
 	import Modal from '../Modal.vue';
 	import io from '../../../io';
+	import validation from '../../../validation';
 
 	export default {
 		components: { Modal },
@@ -30,8 +31,12 @@
 		},
 		methods: {
 			createPlaylist: function () {
-				let _this = this;
-				_this.socket.emit('playlists.create', _this.playlist, res => {
+				const displayName = this.playlist.displayName;
+				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);
+
+
+				this.socket.emit('playlists.create', this.playlist, res => {
 					Toast.methods.addToast(res.message, 3000);
 				});
 				this.$parent.modals.createPlaylist = !this.$parent.modals.createPlaylist;

+ 6 - 0
frontend/components/Modals/Playlists/Edit.vue

@@ -71,6 +71,7 @@
 	import { Toast } from 'vue-roaster';
 	import Modal from '../Modal.vue';
 	import io from '../../../io';
+	import validation from '../../../validation';
 
 	export default {
 		components: { Modal },
@@ -131,6 +132,11 @@
 				});
 			},
 			renamePlaylist: function () {
+				const displayName = this.playlist.displayName;
+				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);
+
+
 				this.socket.emit('playlists.updateDisplayName', this.playlist._id, this.playlist.displayName, res => {
 					Toast.methods.addToast(res.message, 4000);
 				});

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

@@ -49,7 +49,7 @@
 			let _this = this;
 			lofig.get('recaptcha', obj => {
 				_this.recaptcha.key = obj.key;
-				grecaptcha.render('recaptcha', {
+				_this.recaptcha.id = grecaptcha.render('recaptcha', {
 					'sitekey' : _this.recaptcha.key
 				});
 			});
@@ -60,7 +60,7 @@
 				else this.$dispatch('toggleModal', 'register');
 			},
 			submitModal: function () {
-				this.$dispatch('register');
+				this.$dispatch('register', this.recaptcha.id);
 				this.toggleModal();
 			}
 		},

+ 1 - 1
frontend/components/Sidebars/UsersList.vue

@@ -6,7 +6,7 @@
 			<aside class="menu">
 				<ul class="menu-list">
 					<li v-for="user in $parent.users">
-						<a href="#" v-link="{ path: '/u/' + user }" target="_blank">@{{user}}</a>
+						<a href="#" v-link="{ path: '/u/' + user }" target="_blank">{{user}}</a>
 					</li>
 				</ul>
 			</aside>

+ 104 - 28
frontend/components/Station/CommunityHeader.vue

@@ -1,35 +1,61 @@
 <template>
 	<nav class='nav'>
 		<div class='nav-left'>
-			<a class='nav-item logo' href='#' v-link='{ path: "/" }' @click='this.$dispatch("leaveStation", title)'>
+			<a class='nav-item is-brand' href='#' v-link='{ path: "/" }' @click='this.$dispatch("leaveStation", title)'>
 				Musare
 			</a>
-
-
 		</div>
 
 		<div class='nav-center stationDisplayName'>
 			{{$parent.station.displayName}}
 		</div>
 
-		<span class="nav-toggle" :class="{ 'is-active': isMobile }" @click="isMobile = !isMobile">
+		<span class="nav-toggle" @click="controlBar = !controlBar">
 			<span></span>
 			<span></span>
 			<span></span>
 		</span>
 
 		<div class="nav-right nav-menu" :class="{ 'is-active': isMobile }">
-			<!-- DUPLICATE BUTTON TO HOLD FORMATTING -->
-			<a class='nav-item' href='#' @click='$parent.toggleSidebar("users")'>
-				<span class='icon'>
-					<i class='material-icons'>people</i>
-				</span>
+			<a class="nav-item is-tab admin" href="#" v-link="{ path: '/admin' }" v-if="$parent.$parent.role === 'admin'">
+				<strong>Admin</strong>
+			</a>
+			<!--a class="nav-item is-tab" href="#">
+				About
+			</a-->
+			<a class="nav-item is-tab" href="#" v-link="{ path: '/team' }">
+				Team
+			</a>
+			<a class="nav-item is-tab" href="#" v-link="{ path: '/about' }">
+				About
+			</a>
+			<a class="nav-item is-tab" href="#" v-link="{ path: '/news' }">
+				News
 			</a>
+			<span class="grouped" v-if="$parent.$parent.loggedIn">
+				<a class="nav-item is-tab" href="#" v-link="{ path: '/u/' + $parent.$parent.username }">
+					Profile
+				</a>
+				<a class="nav-item is-tab" href="#" v-link="{ path: '/settings' }">
+					Settings
+				</a>
+				<a class="nav-item is-tab" href="#" @click="$parent.$parent.logout()">
+					Logout
+				</a>
+			</span>
+			<span class="grouped" v-else>
+				<a class="nav-item" href="#" @click="toggleModal('login')">
+					Login
+				</a>
+				<a class="nav-item" href="#" @click="toggleModal('register')">
+					Register
+				</a>
+			</span>
 		</div>
+
 	</nav>
-	<div class="admin-sidebar">
+	<div class="control-sidebar" :class="{ 'show-controlBar': controlBar }">
 		<div class='inner-wrapper'>
-			<hr class="sidebar-top-hr">
 			<div v-if='isOwner()'>
 				<a class="sidebar-item" href='#' v-if='isOwner()' @click='$parent.editStation()'>
 					<span class='icon'>
@@ -100,12 +126,16 @@
 		data() {
 			return {
 				title: this.$route.params.id,
-				isMobile: false
+				isMobile: false,
+				controlBar: true
 			}
 		},
 		methods: {
 			isOwner: function () {
 				return this.$parent.$parent.loggedIn && (this.$parent.$parent.role === 'admin' || this.$parent.$parent.userId === this.$parent.station.owner);
+			},
+			toggleModal: function (type) {
+				this.$dispatch('toggleModal', type);
 			}
 		}
 	}
@@ -115,15 +145,27 @@
 	@import 'theme.scss';
 	.nav {
 		background-color: #03a9f4;
+		line-height: 64px;
+
+		.is-brand {
+			font-size: 2.1rem !important;
+			line-height: 64px !important;
+			padding: 0 20px;
+		}
 	}
 
 	a.nav-item {
 		color: $white;
+		font-size: 15px;
 
 		&:hover {
 			color: $white;
 		}
 
+		.admin {
+			color: #424242;
+		}
+
 		padding: 0 18px;
 		.icon {
 			height: 64px;
@@ -136,6 +178,12 @@
 		}
 	}
 
+	.grouped {
+		margin: 0;
+		display: flex;
+		text-decoration: none;
+	}
+
 	.skip-votes {
 		position: relative;
 		left: 11px;
@@ -145,6 +193,21 @@
 		height: 64px;
 	}
 
+	@media screen and (max-width: 998px) {
+		.nav-menu {
+		    background-color: white;
+		    box-shadow: 0 4px 7px rgba(10, 10, 10, 0.1);
+		    left: 0;
+		    display: none;
+		    right: 0;
+		    top: 100%;
+		    position: absolute;
+		}
+		.nav-toggle {
+	    	display: block;
+		}
+	}
+
 	.logo {
 		font-size: 2.1rem;
 		line-height: 64px;
@@ -154,9 +217,14 @@
 
 	.nav-center {
 		display: flex;
-    	align-items: center;
+    align-items: center;
 		color: $blue;
 		font-size: 22px;
+		position: absolute;
+		margin: auto;
+		top: 50%;
+		left: 50%;
+		transform: translate(-50%, -50%);
 	}
 
 	.nav-right.is-active .nav-item {
@@ -164,7 +232,7 @@
     	border: 0;
 	}
 
-	.admin-sidebar {
+	.control-sidebar {
 		position: fixed;
 		z-index: 1;
 		top: 0;
@@ -173,6 +241,14 @@
 		height: 100vh;
 		background-color: #03a9f4;
 		box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
+
+		@media (max-width: 998px) {
+			display: none;
+		}
+	}
+
+	.show-controlBar {
+		display: block;
 	}
 
 	.inner-wrapper {
@@ -180,11 +256,11 @@
 		position: relative;
 	}
 
-	.admin-sidebar .material-icons {
+	.control-sidebar .material-icons {
 		width: 100%;
 		font-size: 2rem;
 	}
-	.admin-sidebar .sidebar-item {
+	.control-sidebar .sidebar-item {
 		font-size: 2rem;
 		height: 50px;
 		color: white;
@@ -205,25 +281,25 @@
 		width: 100%;
 		position: relative;
 	}
-	.admin-sidebar .sidebar-top-hr {
+	.control-sidebar .sidebar-top-hr {
 		margin: 0 0 20px 0;
 	}
 
 	.sidebar-item .icon-purpose {
-    visibility: hidden;
-    width: 150px;
+	    visibility: hidden;
+	    width: 150px;
 		font-size: 12px;
-    background-color: rgba(3, 169, 244,0.8);
-    color: #fff;
-    text-align: center;
-    border-radius: 6px;
-    padding: 5px 0;
-    position: absolute;
-    z-index: 1;
-    left: 105%;
+	    background-color: rgba(3, 169, 244,0.8);
+	    color: #fff;
+	    text-align: center;
+	    border-radius: 6px;
+	    padding: 5px 0;
+	    position: absolute;
+	    z-index: 1;
+	    left: 105%;
 	}
 
 	.sidebar-item:hover .icon-purpose {
-    visibility: visible;
+	    visibility: visible;
 	}
 </style>

+ 112 - 33
frontend/components/Station/OfficialHeader.vue

@@ -1,7 +1,7 @@
 <template>
 	<nav class='nav'>
 		<div class='nav-left'>
-			<a class='nav-item logo' href='#' v-link='{ path: "/" }' @click='this.$dispatch("leaveStation", title)'>
+			<a class='nav-item is-brand' href='#' v-link='{ path: "/" }' @click='this.$dispatch("leaveStation", title)'>
 				Musare
 			</a>
 		</div>
@@ -10,24 +10,52 @@
 			{{ $parent.station.displayName }}
 		</div>
 
-		<span class="nav-toggle" :class="{ 'is-active': isMobile }" @click="isMobile = !isMobile">
+		<span class="nav-toggle" @click="controlBar = !controlBar">
 			<span></span>
 			<span></span>
 			<span></span>
 		</span>
 
 		<div class="nav-right nav-menu" :class="{ 'is-active': isMobile }">
-			<!-- DUPLICATE BUTTON TO HOLD FORMATTING -->
-			<a class='nav-item' href='#' @click='$parent.toggleSidebar("songslist")'>
-				<span class='icon'>
-					<i class='material-icons'>queue_music</i>
-				</span>
+			<a class="nav-item is-tab admin" href="#" v-link="{ path: '/admin' }" v-if="$parent.$parent.role === 'admin'">
+				<strong>Admin</strong>
+			</a>
+			<!--a class="nav-item is-tab" href="#">
+				About
+			</a-->
+			<a class="nav-item is-tab" href="#" v-link="{ path: '/team' }">
+				Team
+			</a>
+			<a class="nav-item is-tab" href="#" v-link="{ path: '/about' }">
+				About
 			</a>
+			<a class="nav-item is-tab" href="#" v-link="{ path: '/news' }">
+				News
+			</a>
+			<span class="grouped" v-if="$parent.$parent.loggedIn">
+				<a class="nav-item is-tab" href="#" v-link="{ path: '/u/' + $parent.$parent.username }">
+					Profile
+				</a>
+				<a class="nav-item is-tab" href="#" v-link="{ path: '/settings' }">
+					Settings
+				</a>
+				<a class="nav-item is-tab" href="#" @click="$parent.$parent.logout()">
+					Logout
+				</a>
+			</span>
+			<span class="grouped" v-else>
+				<a class="nav-item" href="#" @click="toggleModal('login')">
+					Login
+				</a>
+				<a class="nav-item" href="#" @click="toggleModal('register')">
+					Register
+				</a>
+			</span>
 		</div>
+
 	</nav>
-	<div class="admin-sidebar">
+	<div class="control-sidebar" :class="{ 'show-controlBar': controlBar }">
 		<div class='inner-wrapper'>
-			<hr class="sidebar-top-hr">
 			<div v-if='isOwner()'>
 				<a class="sidebar-item" href='#' v-if='isOwner()' @click='$parent.editStation()'>
 					<span class='icon'>
@@ -69,12 +97,6 @@
 					<span class="skip-votes">{{$parent.currentSong.skipVotes}}</span>
 					<span class="icon-purpose">Skip current song</span>
 				</a>
-				<a v-if='$parent.$parent.loggedIn && !$parent.noSong && !$parent.simpleSong' class="sidebar-item" href='#' @click='$parent.modals.report = !$parent.modals.report'>
-					<span class='icon'>
-						<i class='material-icons'>report</i>
-					</span>
-					<span class="icon-purpose">Report a song</span>
-				</a>
 				<a v-if='$parent.$parent.loggedIn && !$parent.noSong' class="sidebar-item" href='#' @click='$parent.modals.addSongToPlaylist = true'>
 					<span class='icon'>
 						<i class='material-icons'>playlist_add</i>
@@ -95,6 +117,13 @@
 				</span>
 				<span class="icon-purpose">Display users in the station</span>
 			</a>
+			<hr>
+			<a v-if='$parent.$parent.loggedIn && !$parent.noSong && !$parent.simpleSong' class="sidebar-item" href='#' @click='$parent.modals.report = !$parent.modals.report'>
+				<span class='icon'>
+					<i class='material-icons'>report</i>
+				</span>
+				<span class="icon-purpose">Report a song</span>
+			</a>
 		</div>
 	</div>
 </template>
@@ -104,12 +133,16 @@
 		data() {
 			return {
 				title: this.$route.params.id,
-				isMobile: false
+				isMobile: false,
+				controlBar: false
 			}
 		},
 		methods: {
 			isOwner: function () {
 				return this.$parent.$parent.loggedIn && this.$parent.$parent.role === 'admin';
+			},
+			toggleModal: function (type) {
+				this.$dispatch('toggleModal', type);
 			}
 		}
 	}
@@ -119,15 +152,27 @@
 	@import 'theme.scss';
 	.nav {
 		background-color: #03a9f4;
+		line-height: 64px;
+
+		.is-brand {
+			font-size: 2.1rem !important;
+			line-height: 64px !important;
+			padding: 0 20px;
+		}
 	}
 
 	a.nav-item {
 		color: $white;
+		font-size: 15px;
 
 		&:hover {
 			color: $white;
 		}
 
+		.admin {
+			color: #424242;
+		}
+
 		padding: 0 18px;
 		.icon {
 			height: 64px;
@@ -140,6 +185,12 @@
 		}
 	}
 
+	.grouped {
+		margin: 0;
+		display: flex;
+		text-decoration: none;
+	}
+
 	.skip-votes {
 		position: relative;
 		left: 11px;
@@ -149,6 +200,21 @@
 		height: 64px;
 	}
 
+	@media screen and (max-width: 998px) {
+		.nav-menu {
+		    background-color: white;
+		    box-shadow: 0 4px 7px rgba(10, 10, 10, 0.1);
+		    left: 0;
+		    display: none;
+		    right: 0;
+		    top: 100%;
+		    position: absolute;
+		}
+		.nav-toggle {
+	    	display: block;
+		}
+	}
+
 	.logo {
 		font-size: 2.1rem;
 		line-height: 64px;
@@ -161,6 +227,11 @@
     	align-items: center;
 		color: $blue;
 		font-size: 22px;
+		position: absolute;
+		margin: auto;
+		top: 50%;
+		left: 50%;
+		transform: translate(-50%, -50%);
 	}
 
 	.nav-right.is-active .nav-item {
@@ -172,7 +243,7 @@
 		display: none;
 	}
 
-	.admin-sidebar {
+	.control-sidebar {
 		position: fixed;
 		z-index: 1;
 		top: 0;
@@ -183,6 +254,14 @@
 		box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
 		overflow-y: auto;
 		overflow-x: hidden;
+
+		@media (max-width: 998px) {
+			display: none;
+		}
+	}
+
+	.show-controlBar {
+		display: block;
 	}
 
 	.inner-wrapper {
@@ -190,32 +269,32 @@
 		position: relative;
 	}
 
-	.admin-sidebar .material-icons {
+	.control-sidebar .material-icons {
 		width: 100%;
 		font-size: 2rem;
 	}
-	.admin-sidebar .sidebar-item {
+	.control-sidebar .sidebar-item {
 		font-size: 2rem;
 		height: 50px;
 		color: white;
 		-webkit-box-align: center;
-    -ms-flex-align: center;
-    align-items: center;
-    display: -webkit-box;
-    display: -ms-flexbox;
-    display: flex;
-    -webkit-box-flex: 0;
-    -ms-flex-positive: 0;
-    flex-grow: 0;
-    -ms-flex-negative: 0;
-    flex-shrink: 0;
-    -webkit-box-pack: center;
-    -ms-flex-pack: center;
-    justify-content: center;
+	    -ms-flex-align: center;
+	    align-items: center;
+	    display: -webkit-box;
+	    display: -ms-flexbox;
+	    display: flex;
+	    -webkit-box-flex: 0;
+	    -ms-flex-positive: 0;
+	    flex-grow: 0;
+	    -ms-flex-negative: 0;
+	    flex-shrink: 0;
+	    -webkit-box-pack: center;
+	    -ms-flex-pack: center;
+	    justify-content: center;
 		width: 100%;
 		position: relative;
 	}
-	.admin-sidebar .sidebar-top-hr {
+	.control-sidebar .sidebar-top-hr {
 		margin: 0 0 20px 0;
 	}
 

+ 12 - 20
frontend/components/Station/Station.vue

@@ -25,19 +25,19 @@
 			<h1 v-if='type === "community" && !station.partyMode && $parent.userId === station.owner && station.privatePlaylist'>Maybe you can add some songs to your selected private playlist and then press the skip button</h1>
 		</div>
 		<div class="columns" v-show="!noSong">
-			<div class="column is-8-desktop is-offset-2-desktop is-11-mobile">
+			<div class="column is-8-desktop is-offset-2-desktop is-12-mobile">
 				<div class="video-container">
 					<div id="player"></div>
-					<div class="seeker-bar-container white" id="preview-progress">
-						<div class="seeker-bar light-blue" style="width: 0%;"></div>
-					</div>
+				</div>
+				<div class="seeker-bar-container white" id="preview-progress">
+					<div class="seeker-bar light-blue" style="width: 0%;"></div>
 				</div>
 			</div>
 		</div>
 		<div class="desktop-only columns is-mobile" v-show="!noSong">
 			<div class="column is-8-desktop is-offset-2-desktop is-12-mobile">
 				<div class="columns is-mobile">
-					<div class="column is-11-desktop" v-bind:class="{'is-7-desktop': !simpleSong}">
+					<div class="column is-12-desktop">
 						<h4 id="time-display">{{timeElapsed}} / {{formatTime(currentSong.duration)}}</h4>
 						<h3>{{currentSong.title}}</h3>
 						<h4 class="thin" style="margin-left: 0">{{currentSong.artists}}</h4>
@@ -66,9 +66,6 @@
 							</div>
 						</div>
 					</div>
-					<div class="column is-4-desktop" v-if="!simpleSong">
-						<img class="image" id="song-thumbnail" style="margin-top: 10px !important" :src="currentSong.thumbnail" alt="Song Thumbnail" onerror="this.src='/assets/notes-transparent.png'" />
-					</div>
 				</div>
 			</div>
 		</div>
@@ -337,20 +334,15 @@
 					this.muted = !this.muted;
 					$("#volumeSlider").val(volume);
 					this.player.setVolume(volume);
-					localStorage.setItem("volume", volume);
+					if (!this.muted) localStorage.setItem("volume", volume);
 				}
 			},
 			increaseVolume: function () {
 				if (this.playerReady) {
 					let previousVolume = parseInt(localStorage.getItem("volume"));
 					let volume = previousVolume + 5;
-					if (previousVolume === 0) {
-						this.muted = false;
-					}
-					if (volume > 100) {
-						volume = 100;
-					}
-					console.log(previousVolume, volume);
+					if (previousVolume === 0) this.muted = false;
+					if (volume > 100) volume = 100;
 					$("#volumeSlider").val(volume);
 					this.player.setVolume(volume);
 					localStorage.setItem("volume", volume);
@@ -665,6 +657,7 @@
 		position: relative;
 		display: flex;
 		align-items: center;
+		.material-icons { user-select: none; }
 	}
 
 	.material-icons { cursor: pointer; }
@@ -721,7 +714,7 @@
 			width: 85%;
 		}
 
-		@media (min-width: 881px) {
+		@media (min-width: 999px) {
 			.mobile-only {
 				display: none;
 			}
@@ -729,8 +722,7 @@
 				display: block;
 			}
 		}
-		@media (max-width: 880px) {
-			margin-left: 64px;
+		@media (max-width: 998px) {
 			.mobile-only {
 				display: block;
 			}
@@ -892,7 +884,7 @@
 
 	.seeker-bar-container {
 		position: relative;
-		height: 5px;
+		height: 7px;
 		display: block;
 		width: 100%;
 		overflow: hidden;

+ 25 - 10
frontend/components/User/Settings.vue

@@ -77,6 +77,7 @@
 
 	import LoginModal from '../Modals/Login.vue'
 	import io from '../../io'
+	import validation from '../../validation';
 
 	export default {
 		data() {
@@ -123,24 +124,34 @@
 		},
 		methods: {
 			changeEmail: function () {
-				if (!this.user.email.address) return Toast.methods.addToast('Email cannot be empty', 8000);
-				this.socket.emit('users.updateEmail', this.$parent.userId, this.user.email.address, res => {
+				const email = this.user.email.address;
+				if (!validation.isLength(email, 3, 254)) return Toast.methods.addToast('Email must have between 3 and 254 characters.', 8000);
+				if (email.indexOf('@') !== email.lastIndexOf('@') || !validation.regex.emailSimple.test(email)) return Toast.methods.addToast('Invalid email format.', 8000);
+
+
+				this.socket.emit('users.updateEmail', this.$parent.userId, email, res => {
 					if (res.status !== 'success') Toast.methods.addToast(res.message, 8000);
 					else Toast.methods.addToast('Successfully changed email address', 4000);
 				});
 			},
 			changeUsername: function () {
-				let _this = this;
-				if (!_this.user.username) return Toast.methods.addToast('Username cannot be empty', 8000);
-				_this.socket.emit('users.updateUsername', this.$parent.userId, _this.user.username, res => {
+				const username = this.user.username;
+				if (!validation.isLength(username, 2, 32)) return Toast.methods.addToast('Username must have between 2 and 32 characters.', 8000);
+				if (!validation.regex.azAZ09_.test(username)) return Toast.methods.addToast('Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _.', 8000);
+
+
+				this.socket.emit('users.updateUsername', this.$parent.userId, username, res => {
 					if (res.status !== 'success') Toast.methods.addToast(res.message, 8000);
 					else Toast.methods.addToast('Successfully changed username', 4000);
 				});
 			},
 			changePassword: function () {
-				let _this = this;
-				if (!_this.newPassword) return Toast.methods.addToast('New password cannot be empty', 8000);
-				_this.socket.emit('users.updatePassword', _this.newPassword, res => {
+				const newPassword = this.newPassword;
+				if (!validation.isLength(newPassword, 6, 200)) return Toast.methods.addToast('Password must have between 6 and 200 characters.', 8000);
+				if (!validation.regex.password.test(newPassword)) return Toast.methods.addToast('Invalid password format. Must have one lowercase letter, one uppercase letter, one number and one special character.', 8000);
+
+
+				this.socket.emit('users.updatePassword', newPassword, res => {
 					if (res.status !== 'success') Toast.methods.addToast(res.message, 8000);
 					else Toast.methods.addToast('Successfully changed password', 4000);
 				});
@@ -163,8 +174,12 @@
 				});
 			},
 			setPassword: function () {
-				if (!this.setNewPassword) return Toast.methods.addToast('Password cannot be empty', 8000);
-				this.socket.emit('users.changePasswordWithCode', this.passwordCode, this.setNewPassword, res => {
+				const newPassword = this.setNewPassword;
+				if (!validation.isLength(newPassword, 6, 200)) return Toast.methods.addToast('Password must have between 6 and 200 characters.', 8000);
+				if (!validation.regex.password.test(newPassword)) return Toast.methods.addToast('Invalid password format. Must have one lowercase letter, one uppercase letter, one number and one special character.', 8000);
+
+
+				this.socket.emit('users.changePasswordWithCode', this.passwordCode, newPassword, res => {
 					Toast.methods.addToast(res.message, 8000);
 				});
 			},

+ 5 - 1
frontend/components/pages/Team.vue

@@ -84,7 +84,7 @@
 				<div class='card column is-6-desktop is-offset-3-desktop is-12-mobile'>
 					<header class='card-header'>
 						<p class='card-header-title'>
-							IIDjShadowII
+							Antonio
 						</p>
 					</header>
 					<div class='card-content'>
@@ -95,6 +95,10 @@
 									<b>Joined: </b>
 									November 11, 2015
 								</li>
+								<li>
+									<b>Email: </b>
+									<a href="&#109;&#097;&#105;&#108;&#116;&#111;:&#108;&#108;&#100;&#106;&#115;&#104;&#097;&#100;&#111;&#119;&#108;&#108;&#064;&#103;&#109;&#097;&#105;&#108;&#046;&#099;&#111;&#109;">&#108;&#108;&#100;&#106;&#115;&#104;&#097;&#100;&#111;&#119;&#108;&#108;&#064;&#103;&#109;&#097;&#105;&#108;&#046;&#099;&#111;&#109;</a>
+								</li>
 							</ul>
 						</div>
 					</div>

+ 6 - 0
frontend/main.js

@@ -45,6 +45,7 @@ document.onkeydown = event => {
 
 router.beforeEach(transition => {
 	window.location.hash = '';
+	//
 	if (window.stationInterval) {
 		clearInterval(window.stationInterval);
 		window.stationInterval = 0;
@@ -82,6 +83,11 @@ router.beforeEach(transition => {
 	}
 });
 
+router.afterEach((data) => {
+	ga('set', 'page', data.to.path);
+	ga('send', 'pageview');
+});
+
 router.map({
 	'/': {
 		component: Home

+ 12 - 0
frontend/validation.js

@@ -0,0 +1,12 @@
+module.exports = {
+	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]+$/
+	},
+	isLength: (string, min, max) => {
+		return !(typeof string !== 'string' || string.length < min || string.length > max);
+	}
+};

+ 8 - 0
windows-start.cmd

@@ -0,0 +1,8 @@
+start "Redis" "startRedis.cmd"
+start "Mongo" "startmongo.cmd"
+cd backend
+start "Backend" "nodemon"
+cd ..
+cd frontend
+start "Frotend" npm run development-watch
+cd ..