Browse Source

Merge pull request #31 from Musare/staging

Updated Backup
Jonathan 8 years ago
parent
commit
b693e6d458

+ 1 - 5
.gitignore

@@ -22,8 +22,4 @@ frontend/build/config/default.json
 npm
 npm
 
 
 # Logs
 # 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
 ####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`
 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.
 	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
 ## Extra
 
 

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

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

+ 12 - 10
backend/logic/logger.js

@@ -1,11 +1,13 @@
 'use strict';
 'use strict';
 
 
+const dir = `${__dirname}/../../log`;
 const fs = require('fs');
 const fs = require('fs');
+const config = require('config');
 let utils;
 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 started;
 let success = 0;
 let success = 0;
@@ -97,8 +99,8 @@ module.exports = {
 		successThisHour++;
 		successThisHour++;
 		getTime((time) => {
 		getTime((time) => {
 			let timeString = `${time.year}-${twoDigits(time.month)}-${twoDigits(time.day)} ${twoDigits(time.hour)}:${twoDigits(time.minute)}:${twoDigits(time.second)}`;
 			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');
 			console.info('\x1b[32m', timeString, 'SUCCESS', '-', type, '-', message, '\x1b[0m');
 		});
 		});
 	},
 	},
@@ -108,8 +110,8 @@ module.exports = {
 		errorThisHour++;
 		errorThisHour++;
 		getTime((time) => {
 		getTime((time) => {
 			let timeString = `${time.year}-${twoDigits(time.month)}-${twoDigits(time.day)} ${twoDigits(time.hour)}:${twoDigits(time.minute)}:${twoDigits(time.second)}`;
 			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');
 			console.warn('\x1b[31m', timeString, 'ERROR', '-', type, '-', message, '\x1b[0m');
 		});
 		});
 	},
 	},
@@ -119,8 +121,8 @@ module.exports = {
 		infoThisHour++;
 		infoThisHour++;
 		getTime((time) => {
 		getTime((time) => {
 			let timeString = `${time.year}-${twoDigits(time.month)}-${twoDigits(time.day)} ${twoDigits(time.hour)}:${twoDigits(time.minute)}:${twoDigits(time.second)}`;
 			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');
 			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();
 				if (timeElapsed <= station.currentSong.duration) return next2();
 				else {
 				else {
 					logger.error("TASK_STATIONS_SKIP_CHECK", `Skipping ${station._id} as it should have skipped already.`);
 					logger.error("TASK_STATIONS_SKIP_CHECK", `Skipping ${station._id} as it should have skipped already.`);
-					stations.skipStation(station._id);
+					Stations.skipStation(station._id);
 					next2();
 					next2();
 				}
 				}
 			}, () => {
 			}, () => {

+ 29 - 1
backend/logic/utils.js

@@ -2,6 +2,7 @@
 
 
 const moment  = require('moment'),
 const moment  = require('moment'),
 	  io      = require('./io'),
 	  io      = require('./io'),
+	  db      = require('./db'),
 	  config  = require('config'),
 	  config  = require('config'),
 	  async	  = require('async'),
 	  async	  = require('async'),
 	  request = require('request'),
 	  request = require('request'),
@@ -422,7 +423,34 @@ module.exports = {
 	getError: (err) => {
 	getError: (err) => {
 		let error = 'An error occurred.';
 		let error = 'An error occurred.';
 		if (typeof err === "string") error = err;
 		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;
 		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",
     "express-session": "^1.14.0",
     "mailgun-js": "^0.8.0",
     "mailgun-js": "^0.8.0",
     "moment": "^2.15.2",
     "moment": "^2.15.2",
-    "mongoose": "^4.6.0",
+    "mongoose": "^4.9.0",
     "oauth": "^0.9.14",
     "oauth": "^0.9.14",
     "passport": "^0.3.2",
     "passport": "^0.3.2",
     "passport-discord": "^0.1.1",
     "passport-discord": "^0.1.1",

+ 1 - 0
docker-compose.yml

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

+ 17 - 2
frontend/App.vue

@@ -17,6 +17,7 @@
 	import RegisterModal from './components/Modals/Register.vue';
 	import RegisterModal from './components/Modals/Register.vue';
 	import auth from './auth';
 	import auth from './auth';
 	import io from './io';
 	import io from './io';
+	import validation from './validation';
 
 
 	export default {
 	export default {
 		replace: false,
 		replace: false,
@@ -84,10 +85,24 @@
 			}
 			}
 		},
 		},
 		events: {
 		events: {
-			'register': function () {
+			'register': function (recaptchaId) {
 				let { register: { email, username, password } } = this;
 				let { register: { email, username, password } } = this;
 				let _this = 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') {
 					if (result.status === 'success') {
 						Toast.methods.addToast(`You have successfully registered.`, 4000);
 						Toast.methods.addToast(`You have successfully registered.`, 4000);
 						if (result.SID) {
 						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/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://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 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>
 </head>
 <body>
 <body>
 	<script src='https://www.google.com/recaptcha/api.js'></script>
 	<script src='https://www.google.com/recaptcha/api.js'></script>

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

@@ -1,17 +1,19 @@
 <template>
 <template>
 	<modal title='Add Song To Playlist'>
 	<modal title='Add Song To Playlist'>
 		<div slot='body'>
 		<div slot='body'>
+			<h4 class="songTitle">{{ $parent.currentSong.title }}</h4>
+			<h5 class="songArtist">{{ $parent.currentSong.artists }}</h5>
 			<aside class="menu">
 			<aside class="menu">
 				<p class="menu-label">
 				<p class="menu-label">
 					Playlists
 					Playlists
 				</p>
 				</p>
 				<ul class="menu-list">
 				<ul class="menu-list">
-					<li v-for='playlist in playlists'>
+					<li v-for='playlist in playlistsArr'>
 						<div class='playlist'>
 						<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>
 								<i class="material-icons">playlist_add_check</i>
 							</span>
 							</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>
 								<i class="material-icons">playlist_add</i>
 							</span>
 							</span>
 							{{ playlist.displayName }}
 							{{ playlist.displayName }}
@@ -32,47 +34,65 @@
 	export default {
 	export default {
 		data() {
 		data() {
 			return {
 			return {
-				playlists: {}
+				playlists: {},
+				playlistsArr: [],
+				songId: null,
+				song: null
 			}
 			}
 		},
 		},
 		methods: {
 		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) {
 			addSongToPlaylist: function (playlistId) {
 				let _this = this;
 				let _this = this;
 				this.socket.emit('playlists.addSongToPlaylist', this.$parent.currentSong.songId, playlistId, res => {
 				this.socket.emit('playlists.addSongToPlaylist', this.$parent.currentSong.songId, playlistId, res => {
 					Toast.methods.addToast(res.message, 4000);
 					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) {
 			removeSongFromPlaylist: function (playlistId) {
 				let _this = this;
 				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);
 					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 () {
 		ready: function () {
 			let _this = this;
 			let _this = this;
+			this.songId = this.$parent.currentSong.songId;
+			this.song = this.$parent.currentSong;
 			io.getSocket((socket) => {
 			io.getSocket((socket) => {
 				_this.socket = socket;
 				_this.socket = socket;
 				_this.socket.emit('playlists.indexForUser', res => {
 				_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 {
 	.icon.is-small {
 		margin-right: 10px !important;
 		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>
 </style>

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

@@ -2,7 +2,7 @@
 	<modal title='Create Community Station'>
 	<modal title='Create Community Station'>
 		<div slot='body'>
 		<div slot='body'>
 			<!-- validation to check if exists http://bulma.io/documentation/elements/form/ -->
 			<!-- 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'>
 			<p class='control'>
 				<input class='input' type='text' placeholder='Name...' v-model='newCommunity.name' autofocus>
 				<input class='input' type='text' placeholder='Name...' v-model='newCommunity.name' autofocus>
 			</p>
 			</p>
@@ -25,6 +25,7 @@
 	import { Toast } from 'vue-roaster';
 	import { Toast } from 'vue-roaster';
 	import Modal from './Modal.vue';
 	import Modal from './Modal.vue';
 	import io from '../../io';
 	import io from '../../io';
+	import validation from '../../validation';
 
 
 	export default {
 	export default {
 		components: { Modal },
 		components: { Modal },
@@ -48,15 +49,32 @@
 				this.$parent.modals.createCommunityStation = !this.$parent.modals.createCommunityStation;
 				this.$parent.modals.createCommunityStation = !this.$parent.modals.createCommunityStation;
 			},
 			},
 			submitModal: function () {
 			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', {
 				this.socket.emit('stations.create', {
-					name: _this.newCommunity.name,
+					name: name,
 					type: 'community',
 					type: 'community',
-					displayName: _this.newCommunity.displayName,
-					description: _this.newCommunity.description
+					displayName: displayName,
+					description: description
 				}, res => {
 				}, res => {
 					if (res.status === 'success') Toast.methods.addToast(`You have added the station successfully`, 4000);
 					if (res.status === 'success') Toast.methods.addToast(`You have added the station successfully`, 4000);
 					else Toast.methods.addToast(res.message, 4000);
 					else Toast.methods.addToast(res.message, 4000);

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

@@ -3,58 +3,36 @@
 		<modal title='Edit Station'>
 		<modal title='Edit Station'>
 			<div slot='body'>
 			<div slot='body'>
 				<label class='label'>Name</label>
 				<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>
 				<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>
 				<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>
 				<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>
 				<button class='button is-danger' @click='deleteStation()' v-if="$parent.type === 'community'">Delete station</button>
 			</div>
 			</div>
 		</modal>
 		</modal>
@@ -65,6 +43,7 @@
 	import { Toast } from 'vue-roaster';
 	import { Toast } from 'vue-roaster';
 	import Modal from './Modal.vue';
 	import Modal from './Modal.vue';
 	import io from '../../io';
 	import io from '../../io';
+	import validation from '../../validation';
 
 
 	export default {
 	export default {
 		data: function() {
 		data: function() {
@@ -81,14 +60,25 @@
 			}
 			}
 		},
 		},
 		methods: {
 		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 () {
 			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 (res.status === 'success') {
-						if (_this.$parent.station) _this.$parent.station.name = _this.editing.name;
+						if (this.$parent.station) _this.$parent.station.name = name;
 						else {
 						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 () {
 			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 (res.status === 'success') {
-						if (_this.$parent.station) _this.$parent.station.displayName = _this.editing.displayName;
+						if (this.$parent.station) _this.$parent.station.displayName = displayName;
 						else {
 						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 () {
 			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 (res.status === 'success') {
-						if (_this.$parent.station) _this.$parent.station.description = _this.editing.description;
+						if (_this.$parent.station) _this.$parent.station.description = description;
 						else {
 						else {
 							_this.$parent.stations.forEach((station, index) => {
 							_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);
 						return Toast.methods.addToast(res.message, 4000);

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

@@ -39,6 +39,7 @@
 	import io from '../../io';
 	import io from '../../io';
 	import { Toast } from 'vue-roaster';
 	import { Toast } from 'vue-roaster';
 	import Modal from './Modal.vue';
 	import Modal from './Modal.vue';
+	import validation from '../../validation';
 
 
 	export default {
 	export default {
 		components: { Modal },
 		components: { Modal },
@@ -49,23 +50,32 @@
 		},
 		},
 		methods: {
 		methods: {
 			updateUsername: function () {
 			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);
 					Toast.methods.addToast(res.message, 4000);
 				});
 				});
 			},
 			},
 			updateEmail: function () {
 			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);
 					Toast.methods.addToast(res.message, 4000);
 				});
 				});
 			},
 			},
 			updateRole: function () {
 			updateRole: function () {
-				let _this = this;
 				this.socket.emit(`users.updateRole`, this.editing._id, this.editing.role, res => {
 				this.socket.emit(`users.updateRole`, this.editing._id, this.editing.role, res => {
 					Toast.methods.addToast(res.message, 4000);
 					Toast.methods.addToast(res.message, 4000);
 					if (
 					if (
 							res.status === 'success' &&
 							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();
 					) location.reload();
 				});
 				});
 			}
 			}

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

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

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

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

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

@@ -6,7 +6,7 @@
 			<aside class="menu">
 			<aside class="menu">
 				<ul class="menu-list">
 				<ul class="menu-list">
 					<li v-for="user in $parent.users">
 					<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>
 					</li>
 				</ul>
 				</ul>
 			</aside>
 			</aside>

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

@@ -1,35 +1,61 @@
 <template>
 <template>
 	<nav class='nav'>
 	<nav class='nav'>
 		<div class='nav-left'>
 		<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
 				Musare
 			</a>
 			</a>
-
-
 		</div>
 		</div>
 
 
 		<div class='nav-center stationDisplayName'>
 		<div class='nav-center stationDisplayName'>
 			{{$parent.station.displayName}}
 			{{$parent.station.displayName}}
 		</div>
 		</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></span>
 			<span></span>
 			<span></span>
 		</span>
 		</span>
 
 
 		<div class="nav-right nav-menu" :class="{ 'is-active': isMobile }">
 		<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>
 			</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>
 		</div>
+
 	</nav>
 	</nav>
-	<div class="admin-sidebar">
+	<div class="control-sidebar" :class="{ 'show-controlBar': controlBar }">
 		<div class='inner-wrapper'>
 		<div class='inner-wrapper'>
-			<hr class="sidebar-top-hr">
 			<div v-if='isOwner()'>
 			<div v-if='isOwner()'>
 				<a class="sidebar-item" href='#' v-if='isOwner()' @click='$parent.editStation()'>
 				<a class="sidebar-item" href='#' v-if='isOwner()' @click='$parent.editStation()'>
 					<span class='icon'>
 					<span class='icon'>
@@ -100,12 +126,16 @@
 		data() {
 		data() {
 			return {
 			return {
 				title: this.$route.params.id,
 				title: this.$route.params.id,
-				isMobile: false
+				isMobile: false,
+				controlBar: true
 			}
 			}
 		},
 		},
 		methods: {
 		methods: {
 			isOwner: function () {
 			isOwner: function () {
 				return this.$parent.$parent.loggedIn && (this.$parent.$parent.role === 'admin' || this.$parent.$parent.userId === this.$parent.station.owner);
 				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';
 	@import 'theme.scss';
 	.nav {
 	.nav {
 		background-color: #03a9f4;
 		background-color: #03a9f4;
+		line-height: 64px;
+
+		.is-brand {
+			font-size: 2.1rem !important;
+			line-height: 64px !important;
+			padding: 0 20px;
+		}
 	}
 	}
 
 
 	a.nav-item {
 	a.nav-item {
 		color: $white;
 		color: $white;
+		font-size: 15px;
 
 
 		&:hover {
 		&:hover {
 			color: $white;
 			color: $white;
 		}
 		}
 
 
+		.admin {
+			color: #424242;
+		}
+
 		padding: 0 18px;
 		padding: 0 18px;
 		.icon {
 		.icon {
 			height: 64px;
 			height: 64px;
@@ -136,6 +178,12 @@
 		}
 		}
 	}
 	}
 
 
+	.grouped {
+		margin: 0;
+		display: flex;
+		text-decoration: none;
+	}
+
 	.skip-votes {
 	.skip-votes {
 		position: relative;
 		position: relative;
 		left: 11px;
 		left: 11px;
@@ -145,6 +193,21 @@
 		height: 64px;
 		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 {
 	.logo {
 		font-size: 2.1rem;
 		font-size: 2.1rem;
 		line-height: 64px;
 		line-height: 64px;
@@ -154,9 +217,14 @@
 
 
 	.nav-center {
 	.nav-center {
 		display: flex;
 		display: flex;
-    	align-items: center;
+    align-items: center;
 		color: $blue;
 		color: $blue;
 		font-size: 22px;
 		font-size: 22px;
+		position: absolute;
+		margin: auto;
+		top: 50%;
+		left: 50%;
+		transform: translate(-50%, -50%);
 	}
 	}
 
 
 	.nav-right.is-active .nav-item {
 	.nav-right.is-active .nav-item {
@@ -164,7 +232,7 @@
     	border: 0;
     	border: 0;
 	}
 	}
 
 
-	.admin-sidebar {
+	.control-sidebar {
 		position: fixed;
 		position: fixed;
 		z-index: 1;
 		z-index: 1;
 		top: 0;
 		top: 0;
@@ -173,6 +241,14 @@
 		height: 100vh;
 		height: 100vh;
 		background-color: #03a9f4;
 		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);
 		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 {
 	.inner-wrapper {
@@ -180,11 +256,11 @@
 		position: relative;
 		position: relative;
 	}
 	}
 
 
-	.admin-sidebar .material-icons {
+	.control-sidebar .material-icons {
 		width: 100%;
 		width: 100%;
 		font-size: 2rem;
 		font-size: 2rem;
 	}
 	}
-	.admin-sidebar .sidebar-item {
+	.control-sidebar .sidebar-item {
 		font-size: 2rem;
 		font-size: 2rem;
 		height: 50px;
 		height: 50px;
 		color: white;
 		color: white;
@@ -205,25 +281,25 @@
 		width: 100%;
 		width: 100%;
 		position: relative;
 		position: relative;
 	}
 	}
-	.admin-sidebar .sidebar-top-hr {
+	.control-sidebar .sidebar-top-hr {
 		margin: 0 0 20px 0;
 		margin: 0 0 20px 0;
 	}
 	}
 
 
 	.sidebar-item .icon-purpose {
 	.sidebar-item .icon-purpose {
-    visibility: hidden;
-    width: 150px;
+	    visibility: hidden;
+	    width: 150px;
 		font-size: 12px;
 		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 {
 	.sidebar-item:hover .icon-purpose {
-    visibility: visible;
+	    visibility: visible;
 	}
 	}
 </style>
 </style>

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

@@ -1,7 +1,7 @@
 <template>
 <template>
 	<nav class='nav'>
 	<nav class='nav'>
 		<div class='nav-left'>
 		<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
 				Musare
 			</a>
 			</a>
 		</div>
 		</div>
@@ -10,24 +10,52 @@
 			{{ $parent.station.displayName }}
 			{{ $parent.station.displayName }}
 		</div>
 		</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></span>
 			<span></span>
 			<span></span>
 		</span>
 		</span>
 
 
 		<div class="nav-right nav-menu" :class="{ 'is-active': isMobile }">
 		<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>
+			<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>
 		</div>
+
 	</nav>
 	</nav>
-	<div class="admin-sidebar">
+	<div class="control-sidebar" :class="{ 'show-controlBar': controlBar }">
 		<div class='inner-wrapper'>
 		<div class='inner-wrapper'>
-			<hr class="sidebar-top-hr">
 			<div v-if='isOwner()'>
 			<div v-if='isOwner()'>
 				<a class="sidebar-item" href='#' v-if='isOwner()' @click='$parent.editStation()'>
 				<a class="sidebar-item" href='#' v-if='isOwner()' @click='$parent.editStation()'>
 					<span class='icon'>
 					<span class='icon'>
@@ -69,12 +97,6 @@
 					<span class="skip-votes">{{$parent.currentSong.skipVotes}}</span>
 					<span class="skip-votes">{{$parent.currentSong.skipVotes}}</span>
 					<span class="icon-purpose">Skip current song</span>
 					<span class="icon-purpose">Skip current song</span>
 				</a>
 				</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'>
 				<a v-if='$parent.$parent.loggedIn && !$parent.noSong' class="sidebar-item" href='#' @click='$parent.modals.addSongToPlaylist = true'>
 					<span class='icon'>
 					<span class='icon'>
 						<i class='material-icons'>playlist_add</i>
 						<i class='material-icons'>playlist_add</i>
@@ -95,6 +117,13 @@
 				</span>
 				</span>
 				<span class="icon-purpose">Display users in the station</span>
 				<span class="icon-purpose">Display users in the station</span>
 			</a>
 			</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>
 	</div>
 	</div>
 </template>
 </template>
@@ -104,12 +133,16 @@
 		data() {
 		data() {
 			return {
 			return {
 				title: this.$route.params.id,
 				title: this.$route.params.id,
-				isMobile: false
+				isMobile: false,
+				controlBar: false
 			}
 			}
 		},
 		},
 		methods: {
 		methods: {
 			isOwner: function () {
 			isOwner: function () {
 				return this.$parent.$parent.loggedIn && this.$parent.$parent.role === 'admin';
 				return this.$parent.$parent.loggedIn && this.$parent.$parent.role === 'admin';
+			},
+			toggleModal: function (type) {
+				this.$dispatch('toggleModal', type);
 			}
 			}
 		}
 		}
 	}
 	}
@@ -119,15 +152,27 @@
 	@import 'theme.scss';
 	@import 'theme.scss';
 	.nav {
 	.nav {
 		background-color: #03a9f4;
 		background-color: #03a9f4;
+		line-height: 64px;
+
+		.is-brand {
+			font-size: 2.1rem !important;
+			line-height: 64px !important;
+			padding: 0 20px;
+		}
 	}
 	}
 
 
 	a.nav-item {
 	a.nav-item {
 		color: $white;
 		color: $white;
+		font-size: 15px;
 
 
 		&:hover {
 		&:hover {
 			color: $white;
 			color: $white;
 		}
 		}
 
 
+		.admin {
+			color: #424242;
+		}
+
 		padding: 0 18px;
 		padding: 0 18px;
 		.icon {
 		.icon {
 			height: 64px;
 			height: 64px;
@@ -140,6 +185,12 @@
 		}
 		}
 	}
 	}
 
 
+	.grouped {
+		margin: 0;
+		display: flex;
+		text-decoration: none;
+	}
+
 	.skip-votes {
 	.skip-votes {
 		position: relative;
 		position: relative;
 		left: 11px;
 		left: 11px;
@@ -149,6 +200,21 @@
 		height: 64px;
 		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 {
 	.logo {
 		font-size: 2.1rem;
 		font-size: 2.1rem;
 		line-height: 64px;
 		line-height: 64px;
@@ -161,6 +227,11 @@
     	align-items: center;
     	align-items: center;
 		color: $blue;
 		color: $blue;
 		font-size: 22px;
 		font-size: 22px;
+		position: absolute;
+		margin: auto;
+		top: 50%;
+		left: 50%;
+		transform: translate(-50%, -50%);
 	}
 	}
 
 
 	.nav-right.is-active .nav-item {
 	.nav-right.is-active .nav-item {
@@ -172,7 +243,7 @@
 		display: none;
 		display: none;
 	}
 	}
 
 
-	.admin-sidebar {
+	.control-sidebar {
 		position: fixed;
 		position: fixed;
 		z-index: 1;
 		z-index: 1;
 		top: 0;
 		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);
 		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-y: auto;
 		overflow-x: hidden;
 		overflow-x: hidden;
+
+		@media (max-width: 998px) {
+			display: none;
+		}
+	}
+
+	.show-controlBar {
+		display: block;
 	}
 	}
 
 
 	.inner-wrapper {
 	.inner-wrapper {
@@ -190,32 +269,32 @@
 		position: relative;
 		position: relative;
 	}
 	}
 
 
-	.admin-sidebar .material-icons {
+	.control-sidebar .material-icons {
 		width: 100%;
 		width: 100%;
 		font-size: 2rem;
 		font-size: 2rem;
 	}
 	}
-	.admin-sidebar .sidebar-item {
+	.control-sidebar .sidebar-item {
 		font-size: 2rem;
 		font-size: 2rem;
 		height: 50px;
 		height: 50px;
 		color: white;
 		color: white;
 		-webkit-box-align: center;
 		-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%;
 		width: 100%;
 		position: relative;
 		position: relative;
 	}
 	}
-	.admin-sidebar .sidebar-top-hr {
+	.control-sidebar .sidebar-top-hr {
 		margin: 0 0 20px 0;
 		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>
 			<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>
 		<div class="columns" v-show="!noSong">
 		<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 class="video-container">
 					<div id="player"></div>
 					<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>
 		</div>
 		</div>
 		<div class="desktop-only columns is-mobile" v-show="!noSong">
 		<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="column is-8-desktop is-offset-2-desktop is-12-mobile">
 				<div class="columns is-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>
 						<h4 id="time-display">{{timeElapsed}} / {{formatTime(currentSong.duration)}}</h4>
 						<h3>{{currentSong.title}}</h3>
 						<h3>{{currentSong.title}}</h3>
 						<h4 class="thin" style="margin-left: 0">{{currentSong.artists}}</h4>
 						<h4 class="thin" style="margin-left: 0">{{currentSong.artists}}</h4>
@@ -66,9 +66,6 @@
 							</div>
 							</div>
 						</div>
 						</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>
 			</div>
 		</div>
 		</div>
@@ -337,20 +334,15 @@
 					this.muted = !this.muted;
 					this.muted = !this.muted;
 					$("#volumeSlider").val(volume);
 					$("#volumeSlider").val(volume);
 					this.player.setVolume(volume);
 					this.player.setVolume(volume);
-					localStorage.setItem("volume", volume);
+					if (!this.muted) localStorage.setItem("volume", volume);
 				}
 				}
 			},
 			},
 			increaseVolume: function () {
 			increaseVolume: function () {
 				if (this.playerReady) {
 				if (this.playerReady) {
 					let previousVolume = parseInt(localStorage.getItem("volume"));
 					let previousVolume = parseInt(localStorage.getItem("volume"));
 					let volume = previousVolume + 5;
 					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);
 					$("#volumeSlider").val(volume);
 					this.player.setVolume(volume);
 					this.player.setVolume(volume);
 					localStorage.setItem("volume", volume);
 					localStorage.setItem("volume", volume);
@@ -665,6 +657,7 @@
 		position: relative;
 		position: relative;
 		display: flex;
 		display: flex;
 		align-items: center;
 		align-items: center;
+		.material-icons { user-select: none; }
 	}
 	}
 
 
 	.material-icons { cursor: pointer; }
 	.material-icons { cursor: pointer; }
@@ -721,7 +714,7 @@
 			width: 85%;
 			width: 85%;
 		}
 		}
 
 
-		@media (min-width: 881px) {
+		@media (min-width: 999px) {
 			.mobile-only {
 			.mobile-only {
 				display: none;
 				display: none;
 			}
 			}
@@ -729,8 +722,7 @@
 				display: block;
 				display: block;
 			}
 			}
 		}
 		}
-		@media (max-width: 880px) {
-			margin-left: 64px;
+		@media (max-width: 998px) {
 			.mobile-only {
 			.mobile-only {
 				display: block;
 				display: block;
 			}
 			}
@@ -892,7 +884,7 @@
 
 
 	.seeker-bar-container {
 	.seeker-bar-container {
 		position: relative;
 		position: relative;
-		height: 5px;
+		height: 7px;
 		display: block;
 		display: block;
 		width: 100%;
 		width: 100%;
 		overflow: hidden;
 		overflow: hidden;

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

@@ -77,6 +77,7 @@
 
 
 	import LoginModal from '../Modals/Login.vue'
 	import LoginModal from '../Modals/Login.vue'
 	import io from '../../io'
 	import io from '../../io'
+	import validation from '../../validation';
 
 
 	export default {
 	export default {
 		data() {
 		data() {
@@ -123,24 +124,34 @@
 		},
 		},
 		methods: {
 		methods: {
 			changeEmail: function () {
 			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);
 					if (res.status !== 'success') Toast.methods.addToast(res.message, 8000);
 					else Toast.methods.addToast('Successfully changed email address', 4000);
 					else Toast.methods.addToast('Successfully changed email address', 4000);
 				});
 				});
 			},
 			},
 			changeUsername: function () {
 			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);
 					if (res.status !== 'success') Toast.methods.addToast(res.message, 8000);
 					else Toast.methods.addToast('Successfully changed username', 4000);
 					else Toast.methods.addToast('Successfully changed username', 4000);
 				});
 				});
 			},
 			},
 			changePassword: function () {
 			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);
 					if (res.status !== 'success') Toast.methods.addToast(res.message, 8000);
 					else Toast.methods.addToast('Successfully changed password', 4000);
 					else Toast.methods.addToast('Successfully changed password', 4000);
 				});
 				});
@@ -163,8 +174,12 @@
 				});
 				});
 			},
 			},
 			setPassword: function () {
 			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);
 					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'>
 				<div class='card column is-6-desktop is-offset-3-desktop is-12-mobile'>
 					<header class='card-header'>
 					<header class='card-header'>
 						<p class='card-header-title'>
 						<p class='card-header-title'>
-							IIDjShadowII
+							Antonio
 						</p>
 						</p>
 					</header>
 					</header>
 					<div class='card-content'>
 					<div class='card-content'>
@@ -95,6 +95,10 @@
 									<b>Joined: </b>
 									<b>Joined: </b>
 									November 11, 2015
 									November 11, 2015
 								</li>
 								</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>
 							</ul>
 						</div>
 						</div>
 					</div>
 					</div>

+ 6 - 0
frontend/main.js

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