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/
 .vagrant/
 
-
 startRedis.cmd
 startMongo.cmd
 .database
 dump.rdb
+npm-debug.log
 
 # Back End
 backend/node_modules/
@@ -20,4 +20,10 @@ frontend/build/bundle.js
 frontend/build/config/default.json
 
 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
 Option 1: (not recommended for Windows users)
  * [Docker](https://www.docker.com/)
- 
+
 Option 2:
  * [NodeJS](https://nodejs.org/en/download/)
  	* nodemon: `npm install -g nodemon`
@@ -41,7 +41,7 @@ Option 2:
 ## Getting Started
 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`
 
@@ -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.  
    	`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).  
+	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.github` values can be obtained by setting up a [GitHub OAuth Application](https://github.com/settings/developers).  
    	`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 `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.  
-   	 
+
 4. `cp frontend/build/config/template.json frontend/build/config/default.json`
 
 	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:
 
    * Docker for Windows / Mac: This is just `localhost`
-   
+
    * Docker ToolBox: The output of `docker-machine ip default`
-   
+
 ####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`
 
 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"
-		
+
 	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.
 
 4. Create a file called `startRedis.cmd` in the main folder with the contents:
 
 		"D:\Redis\redis-server.exe" "D:\Redis\redis.windows.conf"
-		
+
 	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
 
 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
 
    `docker-machine start default && docker-machine ssh default`
-   
+
 3. Tell boot2docker to mount our volume at startup, by appending to its startup script
 	```bash
 	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
 
    `docker-machine restart default`
-   
+
 5. You now should be good to go!
 
 ### 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).
 
-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`;
 
 const async = require('async');
+const fs = require('fs');
 
 const db = require('./logic/db');
 const app = require('./logic/app');
@@ -66,8 +67,19 @@ async.waterfall([
 		if (!config.get("isDocker")) {
 			const express = require('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();
 	}

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

@@ -212,7 +212,7 @@ let lib = {
 	update: hooks.loginRequired((session, playlistId, playlist, cb, userId) => {
 		async.waterfall([
 			(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) => {
@@ -389,7 +389,7 @@ let lib = {
 	updateDisplayName: hooks.loginRequired((session, playlistId, displayName, cb, userId) => {
 		async.waterfall([
 			(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) => {

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

@@ -91,7 +91,7 @@ module.exports = {
 				let $set = {};
 				for (let prop in updatedSong) if (updatedSong[prop] !== song[prop]) $set[prop] = updatedSong[prop]; updated = true;
 				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) => {
 			if (err) {

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

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

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

@@ -236,6 +236,7 @@ module.exports = {
 				});
 			}
 		], (err, stations) => {
+			console.log(err, stations);
 			if (err) {
 				err = utils.getError(err);
 				logger.error("STATIONS_INDEX", `Indexing stations failed. "${err}"`);
@@ -522,7 +523,7 @@ module.exports = {
 	updateName: hooks.ownerRequired((session, stationId, newName, cb) => {
 		async.waterfall([
 			(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) => {
@@ -550,7 +551,7 @@ module.exports = {
 	updateDisplayName: hooks.ownerRequired((session, stationId, newDisplayName, cb) => {
 		async.waterfall([
 			(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) => {
@@ -578,7 +579,7 @@ module.exports = {
 	updateDescription: hooks.ownerRequired((session, stationId, newDescription, cb) => {
 		async.waterfall([
 			(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) => {
@@ -606,7 +607,7 @@ module.exports = {
 	updatePrivacy: hooks.ownerRequired((session, stationId, newPrivacy, cb) => {
 		async.waterfall([
 			(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) => {
@@ -640,7 +641,7 @@ module.exports = {
 			(station, next) => {
 				if (!station) return next('Station not found.');
 				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) => {
@@ -714,7 +715,7 @@ module.exports = {
 				db.models.station.update({_id: stationId}, {$set: {paused: false}, $inc: {timePaused: Date.now() - station.pausedAt}}, next);
 			},
 
-			(next) => {
+			(res, next) => {
 				stations.updateStation(stationId, next);
 			}
 		], (err) => {
@@ -990,7 +991,7 @@ module.exports = {
 			(playlist, next) => {
 				if (!playlist) return next('Playlist not found.');
 				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) => {

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

@@ -168,6 +168,11 @@ module.exports = {
 		async.waterfall([
 
 			// 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) => {
 				request({
 					url: 'https://www.google.com/recaptcha/api/siteverify',
@@ -412,7 +417,7 @@ module.exports = {
 			},
 
 			(next) => {
-				db.models.user.update({ _id: updatingUserId }, {$set: {username: newUsername}}, next);
+				db.models.user.update({ _id: updatingUserId }, {$set: {username: newUsername}}, {runValidators: true}, next);
 			}
 		], (err) => {
 			if (err && err !== true) {
@@ -470,7 +475,7 @@ module.exports = {
 			},
 
 			(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) => {
@@ -517,7 +522,7 @@ module.exports = {
 				else return 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) => {
@@ -554,6 +559,11 @@ module.exports = {
 				next();
 			},
 
+			(next) => {
+				if (!db.passwordValid(newPassword)) return next('Invalid password. Check if it meets all the requirements.');
+				return next();
+			},
+
 			(next) => {
 				bcrypt.genSalt(10, next);
 			},
@@ -573,7 +583,7 @@ module.exports = {
 				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({
 				status: 'success',
 				message: 'Password successfully updated.'
@@ -605,7 +615,7 @@ module.exports = {
 			(user, next) => {
 				let expires = new Date();
 				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) => {
@@ -683,6 +693,11 @@ module.exports = {
 				next();
 			},
 
+			(next) => {
+				if (!db.passwordValid(newPassword)) return next('Invalid password. Check if it meets all the requirements.');
+				return next();
+			},
+
 			(next) => {
 				bcrypt.genSalt(10, next);
 			},
@@ -693,7 +708,7 @@ module.exports = {
 			},
 
 			(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) => {
 			if (err && err !== true) {
@@ -804,7 +819,7 @@ module.exports = {
 			(user, next) => {
 				let expires = new Date();
 				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) => {
@@ -880,6 +895,11 @@ module.exports = {
 				next();
 			},
 
+			(next) => {
+				if (!db.passwordValid(newPassword)) return next('Invalid password. Check if it meets all the requirements.');
+				return next();
+			},
+
 			(next) => {
 				bcrypt.genSalt(10, next);
 			},
@@ -890,7 +910,7 @@ module.exports = {
 			},
 
 			(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) => {
 			if (err && err !== true) {

+ 2 - 2
backend/logic/app.js

@@ -110,7 +110,7 @@ const lib = {
 							(user, next) => {
 								if (!user) return next('User not found.');
 								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);
 									next(null, user, body);
 								});
@@ -216,7 +216,7 @@ const lib = {
 				(user, next) => {
 					if (!user) return next('User not found.');
 					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) => {
 				if (err) {

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

@@ -4,6 +4,18 @@ const mongoose = require('mongoose');
 
 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;
 
 let lib = {
@@ -33,9 +45,69 @@ let lib = {
 				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) => {
-				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 = {
 				song: mongoose.model('song', lib.schemas.song),
@@ -49,6 +121,11 @@ let lib = {
 
 			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) => {
-				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, () => {
 						next(err, playlist);
 					});
@@ -231,7 +231,7 @@ module.exports = {
 			},
 
 		], (err, station) => {
-			if (err && err !== true) cb(err);
+			if (err && err !== true) return cb(err);
 			cb(null, station);
 		});
 	},
@@ -467,4 +467,4 @@ module.exports = {
 		dislikes: -1
 	}
 
-};
+};

+ 2 - 2
frontend/build/index.html

@@ -5,10 +5,10 @@
 
 	<meta charset='UTF-8'>
 	<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='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='60x60' href='/apple-touch-icon-60x60.png?v=06042016'>

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

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

+ 4 - 4
frontend/components/MainFooter.vue

@@ -6,16 +6,16 @@
 					© Copyright Musare 2015 - 2017
 				</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'/>
 					</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'/>
 					</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'/>
 					</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'/>
 					</a>
 				</p>

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

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

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

@@ -43,7 +43,7 @@
 				this.$parent.editPlaylist(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);
 					Toast.methods.addToast(res.message, 4000);
 				});

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

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

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

@@ -9,7 +9,7 @@
 				<!--Remove validation if it's their own without changing-->
 			</p>
 			<p class="control">
-				<button class="button is-success" @click="changeUsername()">Save Changes</button>
+				<button class="button is-success" @click="changeUsername()">Save changes</button>
 			</p>
 		</div>
 		<label class="label">Email</label>
@@ -19,7 +19,7 @@
 				<!--Remove validation if it's their own without changing-->
 			</p>
 			<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>
 		</div>
 		<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"/>
 			<h2 class="has-text-centered username">@{{user.username}}</h2>
 			<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>
 			<nav class="level">
 				<div class="level-item has-text-centered">
@@ -48,7 +48,7 @@
 		},
 		methods: {
 			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);
 					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>
 									<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>
 							</ul>
 						</div>
@@ -47,7 +47,32 @@
 								</li>
 								<li>
 									<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>
 							</ul>
 						</div>