Browse Source

One commit combining 15 lost commits from Kris and Owen from the past 8 days.

KrisVos130 7 years ago
parent
commit
c4c0b20b90

+ 8 - 2
.gitignore

@@ -4,11 +4,11 @@ Thumbs.db
 .idea/
 .idea/
 .vagrant/
 .vagrant/
 
 
-
 startRedis.cmd
 startRedis.cmd
 startMongo.cmd
 startMongo.cmd
 .database
 .database
 dump.rdb
 dump.rdb
+npm-debug.log
 
 
 # Back End
 # Back End
 backend/node_modules/
 backend/node_modules/
@@ -20,4 +20,10 @@ frontend/build/bundle.js
 frontend/build/config/default.json
 frontend/build/config/default.json
 
 
 npm
 npm
-*.log
+
+# Logs
+all.log
+error.log
+info.log
+success.log
+

+ 21 - 12
README.md

@@ -30,7 +30,7 @@ We currently only have 1 backend, 1 MongoDB server and 1 Redis server running fo
 ## Requirements
 ## Requirements
 Option 1: (not recommended for Windows users)
 Option 1: (not recommended for Windows users)
  * [Docker](https://www.docker.com/)
  * [Docker](https://www.docker.com/)
- 
+
 Option 2:
 Option 2:
  * [NodeJS](https://nodejs.org/en/download/)
  * [NodeJS](https://nodejs.org/en/download/)
  	* nodemon: `npm install -g nodemon`
  	* nodemon: `npm install -g nodemon`
@@ -41,7 +41,7 @@ Option 2:
 ## Getting Started
 ## Getting Started
 Once you've installed the required tools:
 Once you've installed the required tools:
 
 
-1. `git clone https://github.com/MusareNode/MusareNode.git`
+1. `git clone https://github.com/Musare/MusareNode.git`
 
 
 2. `cd MusareNode`
 2. `cd MusareNode`
 
 
@@ -54,6 +54,7 @@ Once you've installed the required tools:
    	The `serverPort` should be the port where the backend will listen on, usually `8080` for non-Docker.  
    	The `serverPort` should be the port where the backend will listen on, usually `8080` for non-Docker.  
    	`isDocker` if you are using Docker or not.  
    	`isDocker` if you are using Docker or not.  
    	The `apis.youtube.key` value can be obtained by setting up a [YouTube API Key](https://developers.google.com/youtube/v3/getting-started).  
    	The `apis.youtube.key` value can be obtained by setting up a [YouTube API Key](https://developers.google.com/youtube/v3/getting-started).  
+	To set up a GitHub OAuth Application, you need to fill in some value's. The homepage is the homepage of frontend. The authorization callback url is the backend url with `/auth/github/authorize/callback` added at the end. For example `http://localhost:8080/auth/github/authorize/callback`.
    	The `apis.recaptcha.secret` value can be obtained by setting up a [ReCaptcha Site](https://www.google.com/recaptcha/admin).  
    	The `apis.recaptcha.secret` value can be obtained by setting up a [ReCaptcha Site](https://www.google.com/recaptcha/admin).  
    	The `apis.github` values can be obtained by setting up a [GitHub OAuth Application](https://github.com/settings/developers).  
    	The `apis.github` values can be obtained by setting up a [GitHub OAuth Application](https://github.com/settings/developers).  
    	`apis.discord` is currently not needed.  
    	`apis.discord` is currently not needed.  
@@ -62,7 +63,7 @@ Once you've installed the required tools:
    	The `mongo.url` url should be left alone for Docker, and changed to `mongodb://localhost:27017/musare` for non-Docker.  
    	The `mongo.url` url should be left alone for Docker, and changed to `mongodb://localhost:27017/musare` for non-Docker.  
    	The `cookie.domain` value should be the ip or address you use to access the site, without protocols (http/https), so for example `localhost`.   
    	The `cookie.domain` value should be the ip or address you use to access the site, without protocols (http/https), so for example `localhost`.   
    	The `cookie.secure` value should be `true` for SSL connections, and `false` for normal http connections.  
    	The `cookie.secure` value should be `true` for SSL connections, and `false` for normal http connections.  
-   	 
+
 4. `cp frontend/build/config/template.json frontend/build/config/default.json`
 4. `cp frontend/build/config/template.json frontend/build/config/default.json`
 
 
 	Values:  
 	Values:  
@@ -93,27 +94,35 @@ Now you have different paths here.
    at `http://<docker-machine-ip>:8080/` where `<docker-machine-ip>` can be found below:
    at `http://<docker-machine-ip>:8080/` where `<docker-machine-ip>` can be found below:
 
 
    * Docker for Windows / Mac: This is just `localhost`
    * Docker for Windows / Mac: This is just `localhost`
-   
+
    * Docker ToolBox: The output of `docker-machine ip default`
    * Docker ToolBox: The output of `docker-machine ip default`
-   
+
 ####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.
+
 1. In the main folder, create a folder called `.database`
 1. In the main folder, create a folder called `.database`
 
 
 2. Create a file called `startMongo.cmd` in the main folder with the contents:
 2. Create a file called `startMongo.cmd` in the main folder with the contents:
 
 
 		"C:\Program Files\MongoDB\Server\3.2\bin\mongod.exe" --dbpath "D:\Programming\HTML\MusareNode\.database"
 		"C:\Program Files\MongoDB\Server\3.2\bin\mongod.exe" --dbpath "D:\Programming\HTML\MusareNode\.database"
-		
+
 	Make sure to adjust your paths accordingly.
 	Make sure to adjust your paths accordingly.
-	
+
 3. In the folder where you installed Redis, edit the `redis.windows.conf` file. In there, look for the property `notify-keyspace-events`. Make sure that property is uncommented and has the value `Ex`. It should look like `notify-keyspace-events Ex` when done.
 3. In the folder where you installed Redis, edit the `redis.windows.conf` file. In there, look for the property `notify-keyspace-events`. Make sure that property is uncommented and has the value `Ex`. It should look like `notify-keyspace-events Ex` when done.
 
 
 4. Create a file called `startRedis.cmd` in the main folder with the contents:
 4. Create a file called `startRedis.cmd` in the main folder with the contents:
 
 
 		"D:\Redis\redis-server.exe" "D:\Redis\redis.windows.conf"
 		"D:\Redis\redis-server.exe" "D:\Redis\redis.windows.conf"
-		
+
 	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.
+
+6. In a command prompt with the pwd of frontend, run `npm run development-watch`
+
+7. In a command prompt with the pwd of backend, run `nodemon`
+
 ## Extra
 ## Extra
 
 
 Below is a list of helpful tips / solutions we've collected while developing MusareNode.
 Below is a list of helpful tips / solutions we've collected while developing MusareNode.
@@ -136,7 +145,7 @@ of the following commands to give Docker Toolbox access to those files.
 2. Now start the machine back up and ssh into it
 2. Now start the machine back up and ssh into it
 
 
    `docker-machine start default && docker-machine ssh default`
    `docker-machine start default && docker-machine ssh default`
-   
+
 3. Tell boot2docker to mount our volume at startup, by appending to its startup script
 3. Tell boot2docker to mount our volume at startup, by appending to its startup script
 	```bash
 	```bash
 	sudo tee -a /mnt/sda1/var/lib/boot2docker/profile >/dev/null <<EOF
 	sudo tee -a /mnt/sda1/var/lib/boot2docker/profile >/dev/null <<EOF
@@ -149,7 +158,7 @@ of the following commands to give Docker Toolbox access to those files.
 4. Restart the docker machine so that it uses the new shared folder
 4. Restart the docker machine so that it uses the new shared folder
 
 
    `docker-machine restart default`
    `docker-machine restart default`
-   
+
 5. You now should be good to go!
 5. You now should be good to go!
 
 
 ### Fixing the "couldn't connect to docker daemon" error
 ### Fixing the "couldn't connect to docker daemon" error
@@ -193,4 +202,4 @@ Toast.methods.addToast('', 0);
 
 
 There are multiple ways to contact us. You can send an email to [musaremusic@gmail.com](musaremusic@gmail.com) or [krisvos130@gmail.com](krisvos130@gmail.com).
 There are multiple ways to contact us. You can send an email to [musaremusic@gmail.com](musaremusic@gmail.com) or [krisvos130@gmail.com](krisvos130@gmail.com).
 
 
-You can also message us on [Facebook](https://www.facebook.com/MusareMusic), [Twitter](https://twitter.com/MusareApp) or on our [Discord](https://discord.gg/Y5NxYGP).
+You can also message us on [Facebook](https://www.facebook.com/MusareMusic), [Twitter](https://twitter.com/MusareApp) or on our [Discord](https://discord.gg/Y5NxYGP).

+ 14 - 2
backend/index.js

@@ -3,6 +3,7 @@
 process.env.NODE_CONFIG_DIR = `${__dirname}/config`;
 process.env.NODE_CONFIG_DIR = `${__dirname}/config`;
 
 
 const async = require('async');
 const async = require('async');
+const fs = require('fs');
 
 
 const db = require('./logic/db');
 const db = require('./logic/db');
 const app = require('./logic/app');
 const app = require('./logic/app');
@@ -66,8 +67,19 @@ async.waterfall([
 		if (!config.get("isDocker")) {
 		if (!config.get("isDocker")) {
 			const express = require('express');
 			const express = require('express');
 			const app = express();
 			const app = express();
-			const server = app.listen(80);
-			app.use(express.static(__dirname + "/../frontend/build/"));
+			app.listen(80);
+			const rootDir = __dirname.substr(0, __dirname.lastIndexOf("backend")) + "frontend\\build\\";
+
+			app.get("/*", (req, res) => {
+				const path = req.path;
+				fs.access(rootDir + path, function(err) {
+					if (!err) {
+						res.sendFile(rootDir + path);
+					} else {
+						res.sendFile(rootDir + "index.html");
+					}
+				});
+			});
 		}
 		}
 		next();
 		next();
 	}
 	}

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

@@ -212,7 +212,7 @@ let lib = {
 	update: hooks.loginRequired((session, playlistId, playlist, cb, userId) => {
 	update: hooks.loginRequired((session, playlistId, playlist, cb, userId) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				db.models.playlist.update({ _id: playlistId, createdBy: userId }, playlist, next);
+				db.models.playlist.update({ _id: playlistId, createdBy: userId }, playlist, {runValidators: true}, next);
 			},
 			},
 
 
 			(res, next) => {
 			(res, next) => {
@@ -389,7 +389,7 @@ let lib = {
 	updateDisplayName: hooks.loginRequired((session, playlistId, displayName, cb, userId) => {
 	updateDisplayName: hooks.loginRequired((session, playlistId, displayName, cb, userId) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				db.models.playlist.update({ _id: playlistId, createdBy: userId }, { $set: { displayName } }, next);
+				db.models.playlist.update({ _id: playlistId, createdBy: userId }, { $set: { displayName } }, {runValidators: true}, next);
 			},
 			},
 
 
 			(res, next) => {
 			(res, next) => {

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

@@ -91,7 +91,7 @@ module.exports = {
 				let $set = {};
 				let $set = {};
 				for (let prop in updatedSong) if (updatedSong[prop] !== song[prop]) $set[prop] = updatedSong[prop]; updated = true;
 				for (let prop in updatedSong) if (updatedSong[prop] !== song[prop]) $set[prop] = updatedSong[prop]; updated = true;
 				if (!updated) return next('No properties changed');
 				if (!updated) return next('No properties changed');
-				db.models.queueSong.update({_id: songId}, {$set}, next);
+				db.models.queueSong.update({_id: songId}, {$set}, {runValidators: true}, next);
 			}
 			}
 		], (err) => {
 		], (err) => {
 			if (err) {
 			if (err) {

+ 4 - 4
backend/logic/actions/songs.js

@@ -120,7 +120,7 @@ module.exports = {
 	update: hooks.adminRequired((session, songId, song, cb) => {
 	update: hooks.adminRequired((session, songId, song, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				db.models.song.update({_id: songId}, song, next);
+				db.models.song.update({_id: songId}, song, {runValidators: true}, next);
 			},
 			},
 
 
 			(res, next) => {
 			(res, next) => {
@@ -148,7 +148,7 @@ module.exports = {
 	remove: hooks.adminRequired((session, songId, cb) => {
 	remove: hooks.adminRequired((session, songId, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				db.models.song.remove({songId}, next);
+				db.models.song.remove({_id: songId}, next);
 			},
 			},
 
 
 			(res, next) => {//TODO Check if res gets returned from above
 			(res, next) => {//TODO Check if res gets returned from above
@@ -157,10 +157,10 @@ module.exports = {
 		], (err) => {
 		], (err) => {
 			if (err) {
 			if (err) {
 				err = utils.getError(err);
 				err = utils.getError(err);
-				logger.error("SONGS_UPDATE", `Failed to update song "${songId}". "${err}"`);
+				logger.error("SONGS_UPDATE", `Failed to remove song "${songId}". "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 				return cb({'status': 'failure', 'message': err});
 			}
 			}
-			logger.success("SONGS_UPDATE", `Successfully updated song "${songId}".`);
+			logger.success("SONGS_UPDATE", `Successfully remove song "${songId}".`);
 			cache.pub('song.removed', songId);
 			cache.pub('song.removed', songId);
 			cb({status: 'success', message: 'Song has been successfully updated'});
 			cb({status: 'success', message: 'Song has been successfully updated'});
 		});
 		});

+ 8 - 7
backend/logic/actions/stations.js

@@ -236,6 +236,7 @@ module.exports = {
 				});
 				});
 			}
 			}
 		], (err, stations) => {
 		], (err, stations) => {
+			console.log(err, stations);
 			if (err) {
 			if (err) {
 				err = utils.getError(err);
 				err = utils.getError(err);
 				logger.error("STATIONS_INDEX", `Indexing stations failed. "${err}"`);
 				logger.error("STATIONS_INDEX", `Indexing stations failed. "${err}"`);
@@ -522,7 +523,7 @@ module.exports = {
 	updateName: hooks.ownerRequired((session, stationId, newName, cb) => {
 	updateName: hooks.ownerRequired((session, stationId, newName, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				db.models.station.update({_id: stationId}, {$set: {name: newName}}, next);
+				db.models.station.update({_id: stationId}, {$set: {name: newName}}, {runValidators: true}, next);
 			},
 			},
 
 
 			(res, next) => {
 			(res, next) => {
@@ -550,7 +551,7 @@ module.exports = {
 	updateDisplayName: hooks.ownerRequired((session, stationId, newDisplayName, cb) => {
 	updateDisplayName: hooks.ownerRequired((session, stationId, newDisplayName, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				db.models.station.update({_id: stationId}, {$set: {displayName: newDisplayName}}, next);
+				db.models.station.update({_id: stationId}, {$set: {displayName: newDisplayName}}, {runValidators: true}, next);
 			},
 			},
 
 
 			(res, next) => {
 			(res, next) => {
@@ -578,7 +579,7 @@ module.exports = {
 	updateDescription: hooks.ownerRequired((session, stationId, newDescription, cb) => {
 	updateDescription: hooks.ownerRequired((session, stationId, newDescription, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				db.models.station.update({_id: stationId}, {$set: {description: newDescription}}, next);
+				db.models.station.update({_id: stationId}, {$set: {description: newDescription}}, {runValidators: true}, next);
 			},
 			},
 
 
 			(res, next) => {
 			(res, next) => {
@@ -606,7 +607,7 @@ module.exports = {
 	updatePrivacy: hooks.ownerRequired((session, stationId, newPrivacy, cb) => {
 	updatePrivacy: hooks.ownerRequired((session, stationId, newPrivacy, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				db.models.station.update({_id: stationId}, {$set: {privacy: newPrivacy}}, next);
+				db.models.station.update({_id: stationId}, {$set: {privacy: newPrivacy}}, {runValidators: true}, next);
 			},
 			},
 
 
 			(res, next) => {
 			(res, next) => {
@@ -640,7 +641,7 @@ module.exports = {
 			(station, next) => {
 			(station, next) => {
 				if (!station) return next('Station not found.');
 				if (!station) return next('Station not found.');
 				if (station.partyMode === newPartyMode) return next('The party mode was already ' + ((newPartyMode) ? 'enabled.' : 'disabled.'));
 				if (station.partyMode === newPartyMode) return next('The party mode was already ' + ((newPartyMode) ? 'enabled.' : 'disabled.'));
-				db.models.station.update({_id: stationId}, {$set: {partyMode: newPartyMode}}, next);
+				db.models.station.update({_id: stationId}, {$set: {partyMode: newPartyMode}}, {runValidators: true}, next);
 			},
 			},
 
 
 			(res, next) => {
 			(res, next) => {
@@ -714,7 +715,7 @@ module.exports = {
 				db.models.station.update({_id: stationId}, {$set: {paused: false}, $inc: {timePaused: Date.now() - station.pausedAt}}, next);
 				db.models.station.update({_id: stationId}, {$set: {paused: false}, $inc: {timePaused: Date.now() - station.pausedAt}}, next);
 			},
 			},
 
 
-			(next) => {
+			(res, next) => {
 				stations.updateStation(stationId, next);
 				stations.updateStation(stationId, next);
 			}
 			}
 		], (err) => {
 		], (err) => {
@@ -990,7 +991,7 @@ module.exports = {
 			(playlist, next) => {
 			(playlist, next) => {
 				if (!playlist) return next('Playlist not found.');
 				if (!playlist) return next('Playlist not found.');
 				let currentSongIndex = (playlist.songs.length > 0) ? playlist.songs.length - 1 : 0;
 				let currentSongIndex = (playlist.songs.length > 0) ? playlist.songs.length - 1 : 0;
-				db.models.station.update({_id: stationId}, {$set: {privatePlaylist: playlistId, currentSongIndex: currentSongIndex}}, next);
+				db.models.station.update({_id: stationId}, {$set: {privatePlaylist: playlistId, currentSongIndex: currentSongIndex}}, {runValidators: true}, next);
 			},
 			},
 
 
 			(res, next) => {
 			(res, next) => {

+ 28 - 8
backend/logic/actions/users.js

@@ -168,6 +168,11 @@ module.exports = {
 		async.waterfall([
 		async.waterfall([
 
 
 			// verify the request with google recaptcha
 			// verify the request with google recaptcha
+			(next) => {
+				if (!db.passwordValid(password)) return next('Invalid password. Check if it meets all the requirements.');
+				return next();
+			},
+
 			(next) => {
 			(next) => {
 				request({
 				request({
 					url: 'https://www.google.com/recaptcha/api/siteverify',
 					url: 'https://www.google.com/recaptcha/api/siteverify',
@@ -412,7 +417,7 @@ module.exports = {
 			},
 			},
 
 
 			(next) => {
 			(next) => {
-				db.models.user.update({ _id: updatingUserId }, {$set: {username: newUsername}}, next);
+				db.models.user.update({ _id: updatingUserId }, {$set: {username: newUsername}}, {runValidators: true}, next);
 			}
 			}
 		], (err) => {
 		], (err) => {
 			if (err && err !== true) {
 			if (err && err !== true) {
@@ -470,7 +475,7 @@ module.exports = {
 			},
 			},
 
 
 			(next) => {
 			(next) => {
-				db.models.user.update({_id: updatingUserId}, {$set: {"email.address": newEmail, "email.verified": false, "email.verificationToken": verificationToken}}, next);
+				db.models.user.update({_id: updatingUserId}, {$set: {"email.address": newEmail, "email.verified": false, "email.verificationToken": verificationToken}}, {runValidators: true}, next);
 			},
 			},
 
 
 			(res, next) => {
 			(res, next) => {
@@ -517,7 +522,7 @@ module.exports = {
 				else return next();
 				else return next();
 			},
 			},
 			(next) => {
 			(next) => {
-				db.models.user.update({_id: updatingUserId}, {$set: {role: newRole}}, next);
+				db.models.user.update({_id: updatingUserId}, {$set: {role: newRole}}, {runValidators: true}, next);
 			}
 			}
 
 
 		], (err) => {
 		], (err) => {
@@ -554,6 +559,11 @@ module.exports = {
 				next();
 				next();
 			},
 			},
 
 
+			(next) => {
+				if (!db.passwordValid(newPassword)) return next('Invalid password. Check if it meets all the requirements.');
+				return next();
+			},
+
 			(next) => {
 			(next) => {
 				bcrypt.genSalt(10, next);
 				bcrypt.genSalt(10, next);
 			},
 			},
@@ -573,7 +583,7 @@ module.exports = {
 				return cb({ status: 'failure', message: err });
 				return cb({ status: 'failure', message: err });
 			}
 			}
 
 
-			logger.error("UPDATE_PASSWORD", `User '${userId}' updated their password.`);
+			logger.success("UPDATE_PASSWORD", `User '${userId}' updated their password.`);
 			cb({
 			cb({
 				status: 'success',
 				status: 'success',
 				message: 'Password successfully updated.'
 				message: 'Password successfully updated.'
@@ -605,7 +615,7 @@ module.exports = {
 			(user, next) => {
 			(user, next) => {
 				let expires = new Date();
 				let expires = new Date();
 				expires.setDate(expires.getDate() + 1);
 				expires.setDate(expires.getDate() + 1);
-				db.models.user.findOneAndUpdate({"email.address": user.email.address}, {$set: {"services.password": {set: {code: code, expires}}}}, next);
+				db.models.user.findOneAndUpdate({"email.address": user.email.address}, {$set: {"services.password": {set: {code: code, expires}}}}, {runValidators: true}, next);
 			},
 			},
 
 
 			(user, next) => {
 			(user, next) => {
@@ -683,6 +693,11 @@ module.exports = {
 				next();
 				next();
 			},
 			},
 
 
+			(next) => {
+				if (!db.passwordValid(newPassword)) return next('Invalid password. Check if it meets all the requirements.');
+				return next();
+			},
+
 			(next) => {
 			(next) => {
 				bcrypt.genSalt(10, next);
 				bcrypt.genSalt(10, next);
 			},
 			},
@@ -693,7 +708,7 @@ module.exports = {
 			},
 			},
 
 
 			(hashedPassword, next) => {
 			(hashedPassword, next) => {
-				db.models.user.update({"services.password.set.code": code}, {$set: {"services.password.password": hashedPassword}, $unset: {"services.password.set": ''}}, next);
+				db.models.user.update({"services.password.set.code": code}, {$set: {"services.password.password": hashedPassword}, $unset: {"services.password.set": ''}}, {runValidators: true}, next);
 			}
 			}
 		], (err) => {
 		], (err) => {
 			if (err && err !== true) {
 			if (err && err !== true) {
@@ -804,7 +819,7 @@ module.exports = {
 			(user, next) => {
 			(user, next) => {
 				let expires = new Date();
 				let expires = new Date();
 				expires.setDate(expires.getDate() + 1);
 				expires.setDate(expires.getDate() + 1);
-				db.models.user.findOneAndUpdate({"email.address": email}, {$set: {"services.password.reset": {code: code, expires}}}, next);
+				db.models.user.findOneAndUpdate({"email.address": email}, {$set: {"services.password.reset": {code: code, expires}}}, {runValidators: true}, next);
 			},
 			},
 
 
 			(user, next) => {
 			(user, next) => {
@@ -880,6 +895,11 @@ module.exports = {
 				next();
 				next();
 			},
 			},
 
 
+			(next) => {
+				if (!db.passwordValid(newPassword)) return next('Invalid password. Check if it meets all the requirements.');
+				return next();
+			},
+
 			(next) => {
 			(next) => {
 				bcrypt.genSalt(10, next);
 				bcrypt.genSalt(10, next);
 			},
 			},
@@ -890,7 +910,7 @@ module.exports = {
 			},
 			},
 
 
 			(hashedPassword, next) => {
 			(hashedPassword, next) => {
-				db.models.user.update({"services.password.reset.code": code}, {$set: {"services.password.password": hashedPassword}, $unset: {"services.password.reset": ''}}, next);
+				db.models.user.update({"services.password.reset.code": code}, {$set: {"services.password.password": hashedPassword}, $unset: {"services.password.reset": ''}}, {runValidators: true}, next);
 			}
 			}
 		], (err) => {
 		], (err) => {
 			if (err && err !== true) {
 			if (err && err !== true) {

+ 2 - 2
backend/logic/app.js

@@ -110,7 +110,7 @@ const lib = {
 							(user, next) => {
 							(user, next) => {
 								if (!user) return next('User not found.');
 								if (!user) return next('User not found.');
 								if (user.services.github && user.services.github.id) return next('Account already has GitHub linked.');
 								if (user.services.github && user.services.github.id) return next('Account already has GitHub linked.');
-								db.models.user.update({_id: user._id}, {$set: {"services.github": {id: body.id, access_token}}}, (err) => {
+								db.models.user.update({_id: user._id}, {$set: {"services.github": {id: body.id, access_token}}}, {runValidators: true}, (err) => {
 									if (err) return next(err);
 									if (err) return next(err);
 									next(null, user, body);
 									next(null, user, body);
 								});
 								});
@@ -216,7 +216,7 @@ const lib = {
 				(user, next) => {
 				(user, next) => {
 					if (!user) return next('User not found.');
 					if (!user) return next('User not found.');
 					if (user.email.verified) return next('This email is already verified.');
 					if (user.email.verified) return next('This email is already verified.');
-					db.models.user.update({"email.verificationToken": code}, {$set: {"email.verified": true}, $unset: {"email.verificationToken": ''}}, next);
+					db.models.user.update({"email.verificationToken": code}, {$set: {"email.verified": true}, $unset: {"email.verificationToken": ''}}, {runValidators: true}, next);
 				}
 				}
 			], (err) => {
 			], (err) => {
 				if (err) {
 				if (err) {

+ 79 - 2
backend/logic/db/index.js

@@ -4,6 +4,18 @@ const mongoose = require('mongoose');
 
 
 const bluebird = require('bluebird');
 const bluebird = require('bluebird');
 
 
+const regex = {
+	azAZ09_: /^[A-Za-z0-9_]+$/,
+	az09_: /^[a-z0-9_]+$/,
+	emailSimple: /^[\x00-\x7F]+@[a-z0-9]+\.[a-z0-9]+(\.[a-z0-9]+)?$/,
+	password: /[a-z]+[A-Z]+[0-9]+[^a-zA-Z0-9]+/,
+	ascii: /^[\x00-\x7F]+$/
+};
+
+const isLength = (string, min, max) => {
+	return !(typeof string !== 'string' || string.length < min || string.length > max);
+}
+
 mongoose.Promise = bluebird;
 mongoose.Promise = bluebird;
 
 
 let lib = {
 let lib = {
@@ -33,9 +45,69 @@ let lib = {
 				report: new mongoose.Schema(require(`./schemas/report`))
 				report: new mongoose.Schema(require(`./schemas/report`))
 			};
 			};
 
 
+			lib.schemas.user.path('username').validate((username) => {
+				return (isLength(username, 2, 32) && regex.azAZ09_.test(username));
+			}, 'Invalid username.');
+
+			lib.schemas.user.path('email.address').validate((email) => {
+				if (!isLength(email, 3, 254)) return false;
+				if (email.indexOf('@') !== email.lastIndexOf('@')) return false;
+				return regex.emailSimple.test(email);
+			}, 'Invalid email.');
+
 			lib.schemas.station.path('name').validate((id) => {
 			lib.schemas.station.path('name').validate((id) => {
-				return /^[a-z]+$/.test(id);
-			}, 'The id can only have the letters a-z.');
+				return (isLength(id, 2, 16) && regex.az09_.test(id));
+			}, 'Invalid station name.');
+
+			lib.schemas.station.path('displayName').validate((displayName) => {
+				return (isLength(displayName, 2, 32) && regex.azAZ09_.test(displayName));
+			}, 'Invalid display name.');
+
+			lib.schemas.station.path('description').validate((description) => {
+				if (!isLength(description, 2, 200)) return false;
+				let characters = description.split("");
+				return characters.filter((character) => {
+					console.log(character.charCodeAt(0), character.charCodeAt(0) === 21328);
+					return character.charCodeAt(0) === 21328;
+				}).length === 0;
+			}, 'Invalid display name.');
+
+			let songTitle = (title) => {
+				return (isLength(title, 1, 64) && regex.ascii.test(title));
+			};
+			lib.schemas.song.path('title').validate(songTitle, 'Invalid title.');
+			lib.schemas.queueSong.path('title').validate(songTitle, 'Invalid title.');
+
+			let songArtists = (artists) => {
+				if (artists.length < 1 || artists.length > 10) return false;
+				return artists.filter((artist) => {
+						return (isLength(artist, 1, 32) && regex.ascii.test(artist) && artist !== "NONE");
+					}).length === artists.length;
+			};
+			lib.schemas.song.path('artists').validate(songArtists, 'Invalid artists.');
+			lib.schemas.queueSong.path('artists').validate(songArtists, 'Invalid artists.');
+
+			let songGenres = (genres) => {
+				return genres.filter((genre) => {
+						return (isLength(genre, 1, 16) && regex.az09_.test(genre));
+					}).length === genres.length;
+			};
+			lib.schemas.song.path('genres').validate(songGenres, 'Invalid genres.');
+			lib.schemas.queueSong.path('genres').validate(songGenres, 'Invalid genres.');
+
+			let songThumbnail = (thumbnail) => {
+				return isLength(thumbnail, 8, 256);
+			};
+			lib.schemas.song.path('thumbnail').validate(songThumbnail, 'Invalid thumbnail.');
+			lib.schemas.queueSong.path('thumbnail').validate(songThumbnail, 'Invalid thumbnail.');
+
+			lib.schemas.playlist.path('displayName').validate((displayName) => {
+				return (isLength(displayName, 1, 16) && regex.ascii.test(displayName));
+			}, 'Invalid display name.');
+
+			lib.schemas.report.path('description').validate((description) => {
+				return (!description || (isLength(description, 0, 400) && regex.ascii.test(description)));
+			}, 'Invalid description.');
 
 
 			lib.models = {
 			lib.models = {
 				song: mongoose.model('song', lib.schemas.song),
 				song: mongoose.model('song', lib.schemas.song),
@@ -49,6 +121,11 @@ let lib = {
 
 
 			cb();
 			cb();
 		});
 		});
+	},
+
+	passwordValid: (password) => {
+		if (!isLength(password, 6, 200)) return false;
+		return regex.password.test(password);
 	}
 	}
 };
 };
 
 

+ 3 - 3
backend/logic/stations.js

@@ -167,7 +167,7 @@ module.exports = {
 			},
 			},
 
 
 			(playlist, next) => {
 			(playlist, next) => {
-				db.models.station.update({_id: station._id}, {$set: {playlist: playlist}}, (err) => {
+				db.models.station.update({_id: station._id}, {$set: {playlist: playlist}}, {runValidators: true}, (err) => {
 					_this.updateStation(station._id, () => {
 					_this.updateStation(station._id, () => {
 						next(err, playlist);
 						next(err, playlist);
 					});
 					});
@@ -231,7 +231,7 @@ module.exports = {
 			},
 			},
 
 
 		], (err, station) => {
 		], (err, station) => {
-			if (err && err !== true) cb(err);
+			if (err && err !== true) return cb(err);
 			cb(null, station);
 			cb(null, station);
 		});
 		});
 	},
 	},
@@ -467,4 +467,4 @@ module.exports = {
 		dislikes: -1
 		dislikes: -1
 	}
 	}
 
 
-};
+};

+ 2 - 2
frontend/build/index.html

@@ -5,10 +5,10 @@
 
 
 	<meta charset='UTF-8'>
 	<meta charset='UTF-8'>
 	<meta http-equiv='X-UA-Compatible' content='IE=edge'>
 	<meta http-equiv='X-UA-Compatible' content='IE=edge'>
-	<meta name='viewport' content='width=device-width, initial-scale=1'>
+	<meta name='viewport' content='width=device-width, initial-scale=1, user-scalable=no'>
 	<meta name='keywords' content='music, musare, listen, station, station, radio, edm, chill, community, official, rooms, room, party, good, mus, pop'>
 	<meta name='keywords' content='music, musare, listen, station, station, radio, edm, chill, community, official, rooms, room, party, good, mus, pop'>
 	<meta name='description' content='On Musare you can listen to lots of different songs, playing 24/7 in our official stations and in user-made community stations!'>
 	<meta name='description' content='On Musare you can listen to lots of different songs, playing 24/7 in our official stations and in user-made community stations!'>
-	<meta name='copyright' content='© Copyright Musare 2015-2016 All Right Reserved'>
+	<meta name='copyright' content='© Copyright Musare 2015-2017 All Right Reserved'>
 
 
 	<link rel='apple-touch-icon' sizes='57x57' href='/apple-touch-icon-57x57.png?v=06042016'>
 	<link rel='apple-touch-icon' sizes='57x57' href='/apple-touch-icon-57x57.png?v=06042016'>
 	<link rel='apple-touch-icon' sizes='60x60' href='/apple-touch-icon-60x60.png?v=06042016'>
 	<link rel='apple-touch-icon' sizes='60x60' href='/apple-touch-icon-60x60.png?v=06042016'>

+ 1 - 0
frontend/components/Admin/Songs.vue

@@ -98,6 +98,7 @@
 			},
 			},
 			init: function () {
 			init: function () {
 				let _this = this;
 				let _this = this;
+				_this.songs = [];
 				_this.socket.emit('songs.length', length => {
 				_this.socket.emit('songs.length', length => {
 					_this.maxPosition = Math.round(length / 15);
 					_this.maxPosition = Math.round(length / 15);
 					_this.getSet();
 					_this.getSet();

+ 4 - 4
frontend/components/MainFooter.vue

@@ -6,16 +6,16 @@
 					© Copyright Musare 2015 - 2017
 					© Copyright Musare 2015 - 2017
 				</p>
 				</p>
 				<p>
 				<p>
-					<a class='icon' href='https://github.com/Musare/MusareNode' title='GitHub Repository'>
+					<a class='icon' href='https://github.com/Musare/MusareNode' target='_blank' title='GitHub Repository'>
 						<img src='/assets/social/github.svg'/>
 						<img src='/assets/social/github.svg'/>
 					</a>
 					</a>
-					<a class='icon' href='https://twitter.com/MusareApp' title='Twitter Account'>
+					<a class='icon' href='https://twitter.com/MusareApp' target='_blank' title='Twitter Account'>
 						<img src='/assets/social/twitter.svg'/>
 						<img src='/assets/social/twitter.svg'/>
 					</a>
 					</a>
-					<a class='icon' href='https://www.facebook.com/MusareMusic/' title='Facebook Page'>
+					<a class='icon' href='https://www.facebook.com/MusareMusic/' target='_blank' title='Facebook Page'>
 						<img src='/assets/social/facebook.svg'/>
 						<img src='/assets/social/facebook.svg'/>
 					</a>
 					</a>
-					<a class='icon' href='https://discord.gg/Y5NxYGP' title='Discord Server'>
+					<a class='icon' href='https://discord.gg/Y5NxYGP' target='_blank' title='Discord Server'>
 						<img src='/assets/social/discord.svg'/>
 						<img src='/assets/social/discord.svg'/>
 					</a>
 					</a>
 				</p>
 				</p>

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

@@ -26,7 +26,7 @@
 					</div>
 					</div>
 					&nbsp;&nbsp;Login with GitHub
 					&nbsp;&nbsp;Login with GitHub
 				</a>
 				</a>
-				<a href='#' @click='resetPassword()'>Forgot password?</a>
+				<a href='/reset_password' @click='resetPassword()'>Forgot password?</a>
 			</footer>
 			</footer>
 		</div>
 		</div>
 	</div>
 	</div>

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

@@ -43,7 +43,7 @@
 				this.$parent.editPlaylist(id);
 				this.$parent.editPlaylist(id);
 			},
 			},
 			selectPlaylist: function(id) {
 			selectPlaylist: function(id) {
-				this.socket.emit('stations.selectPrivatePlaylist', this.$parent.stationId, id, (res) => {
+				this.socket.emit('stations.selectPrivatePlaylist', this.$parent.station._id, id, (res) => {
 					if (res.status === 'failure') return Toast.methods.addToast(res.message, 8000);
 					if (res.status === 'failure') return Toast.methods.addToast(res.message, 8000);
 					Toast.methods.addToast(res.message, 4000);
 					Toast.methods.addToast(res.message, 4000);
 				});
 				});

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

@@ -89,6 +89,7 @@
 
 
 	import OfficialHeader from './OfficialHeader.vue';
 	import OfficialHeader from './OfficialHeader.vue';
 	import CommunityHeader from './CommunityHeader.vue';
 	import CommunityHeader from './CommunityHeader.vue';
+	import MainFooter from '../MainFooter.vue';
 	import io from '../../io';
 	import io from '../../io';
 	import auth from '../../auth';
 	import auth from '../../auth';
 
 
@@ -601,7 +602,8 @@
 			Report,
 			Report,
 			SongsListSidebar,
 			SongsListSidebar,
 			PlaylistSidebar,
 			PlaylistSidebar,
-			UsersSidebar
+			UsersSidebar,
+			MainFooter
 		}
 		}
 	}
 	}
 </script>
 </script>

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

@@ -9,7 +9,7 @@
 				<!--Remove validation if it's their own without changing-->
 				<!--Remove validation if it's their own without changing-->
 			</p>
 			</p>
 			<p class="control">
 			<p class="control">
-				<button class="button is-success" @click="changeUsername()">Save Changes</button>
+				<button class="button is-success" @click="changeUsername()">Save changes</button>
 			</p>
 			</p>
 		</div>
 		</div>
 		<label class="label">Email</label>
 		<label class="label">Email</label>
@@ -19,7 +19,7 @@
 				<!--Remove validation if it's their own without changing-->
 				<!--Remove validation if it's their own without changing-->
 			</p>
 			</p>
 			<p class="control is-expanded">
 			<p class="control is-expanded">
-				<button class="button is-success" @click="changeEmail()">Save Changes</button>
+				<button class="button is-success" @click="changeEmail()">Save changes</button>
 			</p>
 			</p>
 		</div>
 		</div>
 		<label class="label" v-if="password">Change Password</label>
 		<label class="label" v-if="password">Change Password</label>

+ 4 - 4
frontend/components/User/Show.vue

@@ -5,9 +5,9 @@
 			<img class="avatar" src="/assets/notes.png"/>
 			<img class="avatar" src="/assets/notes.png"/>
 			<h2 class="has-text-centered username">@{{user.username}}</h2>
 			<h2 class="has-text-centered username">@{{user.username}}</h2>
 			<h5>A member since {{user.createdAt}}</h5>
 			<h5>A member since {{user.createdAt}}</h5>
-			<div class="admin-functionality" v-if="this.$parent.$parent.role === 'admin' == 'admin'">
-				<a class="button is-small is-info is-outlined" href='#' @click="changeRank('admin')" v-if="user.role == 'default'">Promote to Admin</a>
-				<a class="button is-small is-danger is-outlined" href='#' @click="changeRank('default')" v-else>Demote to User</a>
+			<div class="admin-functionality" v-if="$parent.role === 'admin' && !($parent.userId === user._id)">
+				<a class="button is-small is-info is-outlined" @click="changeRank('admin')" v-if="user.role == 'default'">Promote to Admin</a>
+				<a class="button is-small is-danger is-outlined" @click="changeRank('default')" v-if="user.role == 'admin'">Demote to User</a>
 			</div>
 			</div>
 			<nav class="level">
 			<nav class="level">
 				<div class="level-item has-text-centered">
 				<div class="level-item has-text-centered">
@@ -48,7 +48,7 @@
 		},
 		},
 		methods: {
 		methods: {
 			changeRank(newRank) {
 			changeRank(newRank) {
-				this.socket.emit('users.updateRole', this.user._id, 'role', ((newRank == 'admin') ? 'admin' : 'default'), res => {
+				this.socket.emit('users.updateRole', this.user._id, ((newRank == 'admin') ? 'admin' : 'default'), res => {
 					if (res.status == 'error') Toast.methods.addToast(res.message, 2000);
 					if (res.status == 'error') Toast.methods.addToast(res.message, 2000);
 					else this.user.role = newRank; Toast.methods.addToast(`User ${this.$route.params.username}'s rank has been changed to: ${newRank}`, 2000);
 					else this.user.role = newRank; Toast.methods.addToast(`User ${this.$route.params.username}'s rank has been changed to: ${newRank}`, 2000);
 				});
 				});

+ 27 - 2
frontend/components/pages/Team.vue

@@ -22,7 +22,7 @@
 								</li>
 								</li>
 								<li>
 								<li>
 									<b>Email: </b>
 									<b>Email: </b>
-									<a href="mailto:krisvos130@gmail.com">krisvos130@gmail.com</a>
+									<a href="&#109;&#097;&#105;&#108;&#116;&#111;:&#107;&#114;&#105;&#115;&#118;&#111;&#115;&#049;&#051;&#048;&#064;&#103;&#109;&#097;&#105;&#108;&#046;&#099;&#111;&#109;">krisvos130@gmail.com</a>
 								</li>
 								</li>
 							</ul>
 							</ul>
 						</div>
 						</div>
@@ -47,7 +47,32 @@
 								</li>
 								</li>
 								<li>
 								<li>
 									<b>Email: </b>
 									<b>Email: </b>
-									<a href="mailto:atjonathan@engineer.com">atjonathan@engineer.com</a>
+									<a href="&#109;&#097;&#105;&#108;&#116;&#111;:&#097;&#116;&#106;&#111;&#110;&#097;&#116;&#104;&#097;&#110;&#064;&#101;&#110;&#103;&#105;&#110;&#101;&#101;&#114;&#046;&#099;&#111;&#109;">atjonathan@engineer.com</a>
+								</li>
+							</ul>
+						</div>
+					</div>
+				</div>
+			</div>
+			<br>
+			<div class="columns">
+				<div class='card column is-6-desktop is-offset-3-desktop is-12-mobile'>
+					<header class='card-header'>
+						<p class='card-header-title'>
+							Owen Diffey
+						</p>
+					</header>
+					<div class='card-content'>
+						<div class='content'>
+							<span class="custom-tag light-blue">developer</span>
+							<ul>
+								<li>
+									<b>Joined: </b>
+									February 29, 2016
+								</li>
+								<li>
+									<b>Email: </b>
+									<a href="&#109;&#097;&#105;&#108;&#116;&#111;:&#111;&#119;&#101;&#110;&#064;&#111;&#100;&#105;&#102;&#102;&#101;&#121;&#046;&#117;&#107;">&#111;&#119;&#101;&#110;&#064;&#111;&#100;&#105;&#102;&#102;&#101;&#121;&#046;&#117;&#107;</a>
 								</li>
 								</li>
 							</ul>
 							</ul>
 						</div>
 						</div>