Browse Source

Merge branch 'experimental' of github.com:Musare/MusareNode into experimental

Jonathan 5 years ago
parent
commit
e894bd91cc
100 changed files with 3780 additions and 3626 deletions
  1. 34 32
      README.md
  2. 77 0
      backend/core.js
  3. 149 206
      backend/index.js
  4. 11 8
      backend/logic/actions/apis.js
  5. 9 6
      backend/logic/actions/hooks/adminRequired.js
  6. 8 5
      backend/logic/actions/hooks/loginRequired.js
  7. 10 7
      backend/logic/actions/hooks/ownerRequired.js
  8. 16 14
      backend/logic/actions/news.js
  9. 34 32
      backend/logic/actions/playlists.js
  10. 14 11
      backend/logic/actions/punishments.js
  11. 21 18
      backend/logic/actions/queueSongs.js
  12. 19 15
      backend/logic/actions/reports.js
  13. 33 30
      backend/logic/actions/songs.js
  14. 94 90
      backend/logic/actions/stations.js
  15. 88 65
      backend/logic/actions/users.js
  16. 32 26
      backend/logic/api.js
  17. 202 214
      backend/logic/app.js
  18. 81 91
      backend/logic/cache/index.js
  19. 179 182
      backend/logic/db/index.js
  20. 87 91
      backend/logic/discord.js
  21. 147 138
      backend/logic/io.js
  22. 153 178
      backend/logic/logger.js
  23. 30 35
      backend/logic/mail/index.js
  24. 4 1
      backend/logic/mail/schemas/passwordRequest.js
  25. 4 1
      backend/logic/mail/schemas/resetPasswordRequest.js
  26. 4 1
      backend/logic/mail/schemas/verifyEmail.js
  27. 50 58
      backend/logic/notifications.js
  28. 79 74
      backend/logic/playlists.js
  29. 87 79
      backend/logic/punishments.js
  30. 80 72
      backend/logic/songs.js
  31. 61 50
      backend/logic/spotify.js
  32. 194 180
      backend/logic/stations.js
  33. 127 120
      backend/logic/tasks.js
  34. 208 127
      backend/logic/utils.js
  35. 1 1
      backend/package.json
  36. 22 52
      frontend/App.vue
  37. 16 0
      frontend/api/auth.js
  38. 3 4
      frontend/auth.js
  39. 2 0
      frontend/components/404.vue
  40. 36 39
      frontend/components/Admin/EditStation.vue
  41. 22 38
      frontend/components/Admin/News.vue
  42. 12 13
      frontend/components/Admin/Punishments.vue
  43. 39 37
      frontend/components/Admin/QueueSongs.vue
  44. 13 13
      frontend/components/Admin/Reports.vue
  45. 70 42
      frontend/components/Admin/Songs.vue
  46. 31 17
      frontend/components/Admin/Stations.vue
  47. 8 7
      frontend/components/Admin/Statistics.vue
  48. 9 12
      frontend/components/Admin/Users.vue
  49. 6 4
      frontend/components/MainFooter.vue
  50. 22 20
      frontend/components/MainHeader.vue
  51. 35 34
      frontend/components/Modals/AddSongToPlaylist.vue
  52. 34 34
      frontend/components/Modals/AddSongToQueue.vue
  53. 2 5
      frontend/components/Modals/CreateCommunityStation.vue
  54. 41 25
      frontend/components/Modals/EditNews.vue
  55. 57 105
      frontend/components/Modals/EditSong.vue
  56. 26 27
      frontend/components/Modals/EditStation.vue
  57. 9 5
      frontend/components/Modals/EditUser.vue
  58. 2 0
      frontend/components/Modals/IssuesModal.vue
  59. 15 7
      frontend/components/Modals/Login.vue
  60. 4 3
      frontend/components/Modals/MobileAlert.vue
  61. 5 6
      frontend/components/Modals/Playlists/Create.vue
  62. 46 52
      frontend/components/Modals/Playlists/Edit.vue
  63. 23 9
      frontend/components/Modals/Register.vue
  64. 22 27
      frontend/components/Modals/Report.vue
  65. 1 2
      frontend/components/Modals/ViewPunishment.vue
  66. 7 6
      frontend/components/Modals/WhatIsNew.vue
  67. 37 32
      frontend/components/Sidebars/Playlist.vue
  68. 41 40
      frontend/components/Sidebars/SongsList.vue
  69. 18 5
      frontend/components/Sidebars/UsersList.vue
  70. 48 44
      frontend/components/Station/CommunityHeader.vue
  71. 47 51
      frontend/components/Station/OfficialHeader.vue
  72. 259 279
      frontend/components/Station/Station.vue
  73. 4 3
      frontend/components/User/ResetPassword.vue
  74. 33 30
      frontend/components/User/Settings.vue
  75. 14 9
      frontend/components/User/Show.vue
  76. 9 13
      frontend/components/UserIdToUsername.vue
  77. 2 0
      frontend/components/pages/About.vue
  78. 19 17
      frontend/components/pages/Admin.vue
  79. 10 3
      frontend/components/pages/Banned.vue
  80. 152 180
      frontend/components/pages/Home.vue
  81. 17 16
      frontend/components/pages/News.vue
  82. 3 1
      frontend/components/pages/Team.vue
  83. 0 0
      frontend/dist/assets/favicon/android-chrome-144x144.png
  84. 0 0
      frontend/dist/assets/favicon/android-chrome-192x192.png
  85. 0 0
      frontend/dist/assets/favicon/android-chrome-36x36.png
  86. 0 0
      frontend/dist/assets/favicon/android-chrome-48x48.png
  87. 0 0
      frontend/dist/assets/favicon/android-chrome-72x72.png
  88. 0 0
      frontend/dist/assets/favicon/android-chrome-96x96.png
  89. 0 0
      frontend/dist/assets/favicon/apple-touch-icon-114x114.png
  90. 0 0
      frontend/dist/assets/favicon/apple-touch-icon-120x120.png
  91. 0 0
      frontend/dist/assets/favicon/apple-touch-icon-144x144.png
  92. 0 0
      frontend/dist/assets/favicon/apple-touch-icon-152x152.png
  93. 0 0
      frontend/dist/assets/favicon/apple-touch-icon-180x180.png
  94. 0 0
      frontend/dist/assets/favicon/apple-touch-icon-57x57.png
  95. 0 0
      frontend/dist/assets/favicon/apple-touch-icon-60x60.png
  96. 0 0
      frontend/dist/assets/favicon/apple-touch-icon-72x72.png
  97. 0 0
      frontend/dist/assets/favicon/apple-touch-icon-76x76.png
  98. 0 0
      frontend/dist/assets/favicon/apple-touch-icon-precomposed.png
  99. 0 0
      frontend/dist/assets/favicon/apple-touch-icon.png
  100. 0 0
      frontend/dist/assets/favicon/favicon-16x16.png

+ 34 - 32
README.md

@@ -1,4 +1,5 @@
 
+  
 # MusareNode
 
 Based off of the original [Musare](https://github.com/Musare/MusareMeteor), which utilized Meteor.
@@ -56,41 +57,42 @@ Once you've installed the required tools:
 
 3. `cp backend/config/template.json backend/config/default.json`
 
-   Values:
-   The `mode` should be either "development" or "production". No more explanation needed.  
-   The `secret` key can be whatever. It's used by express's session module.  
-   The `domain` should be the url where the site will be accessible from, usually `http://localhost` for non-Docker.  
-   The `serverDomain` should be the url where the backend will be accessible from, usually `http://localhost:8080` for non-Docker.  
-   The `serverPort` should be the port where the backend will listen on, should always be `8080` for Docker, and is recommended 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). You need to use the YouTube Data API v3, and create an API key.  
-   The `apis.recaptcha.secret` value can be obtained by setting up a [ReCaptcha Site (v3)](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). You need to fill in some values to create the OAuth application. 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.discord.token` is the token for the Discord bot.  
-   The `apis.discord.loggingServer` is the Discord logging server id.  
-   The `apis.discord.loggingChannel` is the Discord logging channel id.  
-   The `apis.mailgun` values can be obtained by setting up a [Mailgun account](http://www.mailgun.com/), or you can disable it.  
-   The `apis.spotify` values can be obtained by setting up a [Spotify client id](https://developer.spotify.com/dashboard/applications), or you can disable it.  
-   The `redis.url` url should be left alone for Docker, and changed to `redis://localhost:6379/0` for non-Docker.  
-   The `redis.password` should be the Redis password you either put in your `startRedis.cmd` file for Windows, or `.env` for docker.  
-   The `mongo.url` needs to have the proper password for the MongoDB musare user, and for non-Docker you need to replace `@musare:27017` with `@localhost:27017`.  
-   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.  
+|Property|Description|
+|--|--|
+|`mode`|Should be either `development` or `production`. No more explanation needed.|
+|`secret`|Whatever you want - used by express's session module.|
+|`domain`|Should be the url where the site will be accessible from,usually `http://localhost` for non-Docker.|
+|`serverDomain`|Should be the url where the backend will be accessible from, usually `http://localhost:8080` for non-Docker.|
+|`serverPort`|Should be the port where the backend will listen on, should always be `8080` for Docker, and is recommended for non-Docker.|
+|`isDocker`|Self-explanatory. Are you using Docker?|
+|`serverPort`|Should be the port where the backend will listen on, should always be `8080` for Docker, and is recommended for non-Docker.|
+|`apis.youtube.key`|Can be obtained by setting up a [YouTube API Key](https://developers.google.com/youtube/v3/getting-started). You need to use the YouTube Data API v3, and create an API key.|
+|`apis.recaptcha.secret`|Can be obtained by setting up a [ReCaptcha Site (v3)](https://www.google.com/recaptcha/admin).|
+|`apis.github`|Can be obtained by setting up a [GitHub OAuth Application](https://github.com/settings/developers). You need to fill in some values to create the OAuth application. 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`.|
+|`apis.discord.token`|Token for the Discord bot.|
+|`apis.discord.loggingServer`|Server ID of the Discord logging server.|
+|`apis.discord.loggingChannel`|ID of the channel to be used in the Discord logging server.|
+|`apis.mailgun`|Can be obtained by setting up a [Mailgun account](http://www.mailgun.com/), or you can disable it.|
+|`apis.spotify`|Can be obtained by setting up a [Spotify client id](https://developer.spotify.com/dashboard/applications), or you can disable it.|
+|`redis.url`|Should be left alone for Docker, and changed to `redis://localhost:6379/0` for non-Docker.|
+|`redis.password`|Should be the Redis password you either put in your `startRedis.cmd` file for Windows, or `.env` for docker.|
+|`mongo.url`|Needs to have the proper password for the MongoDB musare user, and for non-Docker you need to replace `@musare:27017` with `@localhost:27017`.|
+|`cookie.domain`|Should be the ip or address you use to access the site, without protocols (http/https), so for example `localhost`.|
+|`cookie.secure`|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:  
-   The `serverDomain` should be the url where the backend will be accessible from, usually `http://localhost:8080` for non-Docker.
-   The `frontendDomain` should be the url where the frontend will be accessible from, usually `http://localhost` for docker or `http://localhost:80` for non-Docker.
-   The `frontendPort` should be the port where the frontend will be accessible from, should always be port `81` for Docker, and is recommended to be port `80` for non-Docker.
-   The `recaptcha.key` value can be obtained by setting up a [ReCaptcha Site (v3)](https://www.google.com/recaptcha/admin).
-   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 `siteSettings.logo` should be the path to the logo image, by default it is `/assets/wordmark.png`.
-   The `siteSettings.siteName` should be the name of the site.
-   The `siteSettings.socialLinks.` `github`,`twitter`,`facebook` and `github` are set to the official Musare accounts by default but can be changed. 
-
-Now you have different paths here.
+|Property|Description|
+|--|--|
+|`serverDomain`|Should be the url where the backend will be accessible from, usually `http://localhost:8080` for non-Docker.|
+|`frontendDomain`|Should be the url where the frontend will be accessible from, usually `http://localhost` for docker or `http://localhost:80` for non-Docker.|
+|`frontendPort`|Should be the port where the frontend will be accessible from, should always be port `81` for Docker, and is recommended to be port `80` for non-Docker.|
+|`recaptcha.key`|Can be obtained by setting up a [ReCaptcha Site (v3)](https://www.google.com/recaptcha/admin).|
+|`cookie.domain`|Should be the ip or address you use to access the site, without protocols (http/https), so for example `localhost`.|
+|`cookie.secure`|Should be `true` for SSL connections, and `false` for normal http connections.|
+|`siteSettings.logo`|Path to the logo image, by default it is `/assets/wordmark.png`.|
+|`siteSettings.siteName`|Should be the name of the site.|
+|`siteSettings.socialLinks`|`github`, `twitter` and `facebook` are set to the official Musare accounts by default, but can be changed.|
 
 ### Installing with Docker
 

+ 77 - 0
backend/core.js

@@ -0,0 +1,77 @@
+const EventEmitter = require('events');
+
+const bus = new EventEmitter();
+
+module.exports = class {
+	constructor(name, moduleManager) {
+		this.name = name;
+		this.moduleManager = moduleManager;
+		this.lockdown = false;
+		this.dependsOn = [];
+		this.eventHandlers = [];
+		this.state = "NOT_INITIALIZED";
+		this.stage = 0;
+		this.lastTime = 0;
+		this.totalTimeInitialize = 0;
+		this.timeDifferences = [];
+	}
+
+	_initialize() {
+		this.logger = this.moduleManager.modules["logger"];
+		this.setState("INITIALIZING");
+
+		this.initialize().then(() => {
+			this.setState("INITIALIZED");
+			this.setStage(0);
+			this.moduleManager.printStatus();
+		}).catch(() => {
+			this.moduleManager.aModuleFailed(this);
+		});
+	}
+
+	_onInitialize() {
+		return new Promise(resolve => bus.once(`stateChange:${this.name}:INITIALIZED`, resolve));
+	}
+
+	_isInitialized() {
+		return new Promise(resolve => {
+			if (this.state === "INITIALIZED") resolve();
+		});
+	}
+
+	_isNotLocked() {
+		return new Promise((resolve, reject) => {
+			if (this.state === "LOCKDOWN") reject();
+			else resolve();
+		});
+	}
+
+	setState(state) {
+		this.state = state;
+		bus.emit(`stateChange:${this.name}:${state}`);
+		this.logger.info(`MODULE_STATE`, `${state}: ${this.name}`);
+	}
+
+	setStage(stage) {
+		if (stage !== 1)
+			this.totalTimeInitialize += (Date.now() - this.lastTime);
+		//this.timeDifferences.push(this.stage + ": " + (Date.now() - this.lastTime) + "ms");
+		this.timeDifferences.push(Date.now() - this.lastTime);
+
+		this.lastTime = Date.now();
+		this.stage = stage;
+		this.moduleManager.printStatus();
+	}
+
+	_validateHook() {
+		return Promise.race([this._onInitialize, this._isInitialized]).then(
+			() => this._isNotLocked()
+		);
+	}
+
+	_lockdown() {
+		this.lockdown = true;
+		this.setState("LOCKDOWN");
+		this.moduleManager.printStatus();
+	}
+}

+ 149 - 206
backend/index.js

@@ -1,223 +1,166 @@
 'use strict';
 
-process.env.NODE_CONFIG_DIR = `${__dirname}/config`;
+const util = require("util");
 
-const async = require('async');
-const fs = require('fs');
-
-//const Discord = require("discord.js");
-//const client = new Discord.Client();
-const db = require('./logic/db');
-const app = require('./logic/app');
-const mail = require('./logic/mail');
-const api = require('./logic/api');
-const io = require('./logic/io');
-const stations = require('./logic/stations');
-const songs = require('./logic/songs');
-const spotify = require('./logic/spotify');
-const playlists = require('./logic/playlists');
-const cache = require('./logic/cache');
-const discord = require('./logic/discord');
-const notifications = require('./logic/notifications');
-const punishments = require('./logic/punishments');
-const logger = require('./logic/logger');
-const tasks = require('./logic/tasks');
-const config = require('config');
-
-let currentComponent;
-let initializedComponents = [];
-let lockdownB = false;
+process.env.NODE_CONFIG_DIR = `${__dirname}/config`;
 
 process.on('uncaughtException', err => {
-	if (lockdownB || err.code === 'ECONNREFUSED' || err.code === 'UNCERTAIN_STATE') return;
+	if (err.code === 'ECONNREFUSED' || err.code === 'UNCERTAIN_STATE') return;
 	console.log(`UNCAUGHT EXCEPTION: ${err.stack}`);
 });
 
-const getError = (err) => {
-	let error = 'An error occurred.';
-	if (typeof err === "string") error = err;
-	else if (err.message) {
-		if (err.message !== 'Validation failed') error = err.message;
-		else error = err.errors[Object.keys(err.errors)].message;
+class ModuleManager {
+	constructor() {
+		this.modules = {};
+		this.modulesInitialized = 0;
+		this.totalModules = 0;
+		this.modulesLeft = [];
+		this.i = 0;
+		this.lockdown = false;
 	}
-	return error;
-};
-
-function lockdown() {
-	if (lockdownB) return;
-	lockdownB = true;
-	initializedComponents.forEach((component) => {
-		component._lockdown();
-	});
-	console.log("Backend locked down.");
-}
 
-function errorCb(message, err, component) {
-	err = getError(err);
-	lockdown();
-	discord.sendAdminAlertMessage(message, "#FF0000", message, true, [{name: "Error:", value: err, inline: false}, {name: "Component:", value: component, inline: true}]); //TODO Maybe due to lockdown this won't work, and what if the Discord module was the one that failed?
-}
+	addModule(moduleName) {
+		console.log("add module", moduleName);
+		const moduleClass = new require(`./logic/${moduleName}`);
+		this.modules[moduleName] = new moduleClass(moduleName, this);
+		this.totalModules++;
+		this.modulesLeft.push(moduleName);
+	}
 
-function moduleStartFunction() {
-	logger.info("MODULE_START", `Starting to initialize component '${currentComponent}'`);
-}
+	initialize() {
+		if (!this.modules["logger"]) return console.error("There is no logger module");
+		this.logger = this.modules["logger"];
+		console.log = (...args) => this.logger.debug(args.map(arg => util.format(arg)));
+		console.debug = (...args) => this.logger.debug(args.map(arg => util.format(arg)));
+		console.info = (...args) => this.logger.debug(args.map(arg => util.format(arg)));
+		console.warn = (...args) => this.logger.debug(args.map(arg => util.format(arg)));
+		console.error = (...args) => this.logger.error("CONSOLE", args.map(arg => util.format(arg)));
+		this.logger.reservedLines = Object.keys(this.modules).length + 5;
+		
+		for (let moduleName in this.modules) {
+			let module = this.modules[moduleName];
+			if (this.lockdown) break;
+
+			module._onInitialize().then(() => {
+				this.moduleInitialized(moduleName);
+			});
+
+			let dependenciesInitializedPromises = [];
+			
+			module.dependsOn.forEach(dependencyName => {
+				let dependency = this.modules[dependencyName];
+				dependenciesInitializedPromises.push(dependency._onInitialize());
+			});
 
-async.waterfall([
-
-	// setup our Discord module
-	(next) => {
-		currentComponent = 'Discord';
-		moduleStartFunction();
-		discord.init(config.get('apis.discord').token, config.get('apis.discord').loggingChannel, errorCb, () => {
-			next();
-		});
-	},
-
-	// setup our Redis cache
-	(next) => {
-		currentComponent = 'Cache';
-		moduleStartFunction();
-		cache.init(config.get('redis').url, config.get('redis').password, errorCb, () => {
-			next();
-		});
-	},
-
-	// setup our MongoDB database
-	(next) => {
-		initializedComponents.push(cache);
-		currentComponent = 'DB';
-		moduleStartFunction();
-		db.init(config.get("mongo").url, errorCb, next);
-	},
-
-	// setup the express server
-	(next) => {
-		initializedComponents.push(db);
-		currentComponent = 'App';
-		moduleStartFunction();
-		app.init(next);
-	},
-
-	// setup the mail
-	(next) => {
-		initializedComponents.push(app);
-		currentComponent = 'Mail';
-		moduleStartFunction();
-		mail.init(next);
-	},
-
-	// setup the Spotify
-	(next) => {
-		initializedComponents.push(mail);
-		currentComponent = 'Spotify';
-		moduleStartFunction();
-		spotify.init(next);
-	},
-
-	// setup the socket.io server (all client / server communication is done over this)
-	(next) => {
-		initializedComponents.push(spotify);
-		currentComponent = 'IO';
-		moduleStartFunction();
-		io.init(next);
-	},
-
-	// setup the punishment system
-	(next) => {
-		initializedComponents.push(io);
-		currentComponent = 'Punishments';
-		moduleStartFunction();
-		punishments.init(next);
-	},
-
-	// setup the notifications
-	(next) => {
-		initializedComponents.push(punishments);
-		currentComponent = 'Notifications';
-		moduleStartFunction();
-		notifications.init(config.get('redis').url, config.get('redis').password, errorCb, next);
-	},
-
-	// setup the stations
-	(next) => {
-		initializedComponents.push(notifications);
-		currentComponent = 'Stations';
-		moduleStartFunction();
-		stations.init(next)
-	},
-
-	// setup the songs
-	(next) => {
-		initializedComponents.push(stations);
-		currentComponent = 'Songs';
-		moduleStartFunction();
-		songs.init(next)
-	},
-
-	// setup the playlists
-	(next) => {
-		initializedComponents.push(songs);
-		currentComponent = 'Playlists';
-		moduleStartFunction();
-		playlists.init(next)
-	},
-
-	// setup the API
-	(next) => {
-		initializedComponents.push(playlists);
-		currentComponent = 'API';
-		moduleStartFunction();
-		api.init(next)
-	},
-
-	// setup the logger
-	(next) => {
-		initializedComponents.push(api);
-		currentComponent = 'Logger';
-		moduleStartFunction();
-		logger.init(next)
-	},
-
-	// setup the tasks system
-	(next) => {
-		initializedComponents.push(logger);
-		currentComponent = 'Tasks';
-		moduleStartFunction();
-		tasks.init(next)
-	},
-
-	// setup the frontend for local setups
-	(next) => {
-		initializedComponents.push(tasks);
-		currentComponent = 'Windows';
-		moduleStartFunction();
-		if (!config.get("isDocker") && !(config.get("mode") === "development" || config.get("mode") === "dev")) {
-			const express = require('express');
-			const app = express();
-			app.listen(config.get("frontendPort"));
-			const rootDir = __dirname.substr(0, __dirname.lastIndexOf("backend")) + "frontend/dist/build";
-
-			app.use(express.static(rootDir, {
-				setHeaders: function(res, path) {
-					if (path.indexOf('.html') !== -1) res.setHeader('Cache-Control', 'public, max-age=0');
-					else res.setHeader('Cache-Control', 'public, max-age=2628000');
-				}
-			}));
-
-			app.get("/*", (req, res) => {
-				res.sendFile(`${rootDir}/index.html`);
+			module.lastTime = Date.now();
+
+			Promise.all(dependenciesInitializedPromises).then((res, res2) => {
+				if (this.lockdown) return;
+				this.logger.info("MODULE_MANAGER", `${moduleName} dependencies have been completed`);
+				module._initialize();
 			});
 		}
-		if (lockdownB) return;
-		next();
 	}
-], (err) => {
-	if (err && err !== true) {
-		lockdown();
-		discord.sendAdminAlertMessage("An error occurred while initializing the backend server.", "#FF0000", "Startup error", true, [{name: "Error:", value: err, inline: false}, {name: "Component:", value: currentComponent, inline: true}]);
-		console.error('An error occurred while initializing the backend server');
-	} else {
-		discord.sendAdminAlertMessage("The backend server started successfully.", "#00AA00", "Startup", false, []);
-		console.info('Backend server has been successfully started');
+
+	async printStatus() {
+		try { await Promise.race([this.logger._onInitialize, this.logger._isInitialized]); } catch { return; }
+		
+		let colors = this.logger.colors;
+
+		const rows = process.stdout.rows;
+
+		process.stdout.cursorTo(0, rows - this.logger.reservedLines);
+		process.stdout.clearScreenDown();
+
+		process.stdout.cursorTo(0, (rows - this.logger.reservedLines) + 2);
+
+		process.stdout.write(`${colors.FgYellow}Modules${colors.FgWhite}:\n`);
+
+		for (let moduleName in this.modules) {
+			let module = this.modules[moduleName];
+			let tabsAmount = 2 - (moduleName.length / 8);
+			
+			let tabs = "";
+			for(let i = 0; i < tabsAmount; i++)
+				tabs += "\t";
+
+			let timing = module.timeDifferences.map((timeDifference) => {
+				return `${colors.FgMagenta}${timeDifference}${colors.FgCyan}ms${colors.FgWhite}`;
+			}).join(", ");
+
+			let stateColor;
+			if (module.state === "NOT_INITIALIZED") stateColor = colors.FgWhite;
+			if (module.state === "INITIALIZING") stateColor = colors.FgYellow;
+			if (module.state === "INITIALIZED") stateColor = colors.FgGreen;
+			if (module.state === "LOCKDOWN") stateColor = colors.FgRed;
+			
+			process.stdout.write(`${moduleName}${tabs}${stateColor}${module.state}\t${colors.FgYellow}Stage: ${colors.FgRed}${module.stage}${colors.FgWhite}. ${colors.FgYellow}Timing${colors.FgWhite}: [${timing}]${colors.FgWhite}${colors.FgWhite}. ${colors.FgYellow}Total time${colors.FgWhite}: ${colors.FgRed}${module.totalTimeInitialize}${colors.FgCyan}ms${colors.Reset}\n`);
+		}
+	}
+
+	moduleInitialized(moduleName) {
+		this.modulesInitialized++;
+		this.modulesLeft.splice(this.modulesLeft.indexOf(moduleName), 1);
+
+		this.logger.info("MODULE_MANAGER", `Initialized: ${this.modulesInitialized}/${this.totalModules}.`);
+
+		if (this.modulesLeft.length === 0) this.allModulesInitialized();
+	}
+
+	allModulesInitialized() {
+		this.logger.success("MODULE_MANAGER", "All modules have started!");
+		this.modules["discord"].sendAdminAlertMessage("The backend server started successfully.", "#00AA00", "Startup", false, []);
+	}
+
+	aModuleFailed(failedModule) {
+		this.logger.error("MODULE_MANAGER", `A module has failed, locking down. Module: ${failedModule.name}`);
+		this.modules["discord"].sendAdminAlertMessage(`The backend server failed to start due to a failing module: ${failedModule.name}.`, "#AA0000", "Startup", false, []);
+
+		this._lockdown();
+	}
+
+	_lockdown() {
+		this.lockdown = true;
+		
+		for (let moduleName in this.modules) {
+			let module = this.modules[moduleName];
+			module._lockdown();
+		}
 	}
+}
+
+const moduleManager = new ModuleManager();
+
+module.exports = moduleManager;
+
+moduleManager.addModule("cache");
+moduleManager.addModule("db");
+moduleManager.addModule("mail");
+moduleManager.addModule("api");
+moduleManager.addModule("app");
+moduleManager.addModule("discord");
+moduleManager.addModule("io");
+moduleManager.addModule("logger");
+moduleManager.addModule("notifications");
+moduleManager.addModule("playlists");
+moduleManager.addModule("punishments");
+moduleManager.addModule("songs");
+moduleManager.addModule("spotify");
+moduleManager.addModule("stations");
+moduleManager.addModule("tasks");
+moduleManager.addModule("utils");
+
+moduleManager.initialize();
+
+process.stdin.on("data", function (data) {
+    if(data.toString() === "lockdown\r\n"){
+        console.log("Locking down.");
+       	moduleManager._lockdown();
+    }
 });
+
+const rows = process.stdout.rows;
+
+for(let i = 0; i < rows; i++) {
+	process.stdout.write("\n");
+}

+ 11 - 8
backend/logic/actions/apis.js

@@ -1,11 +1,14 @@
 'use strict';
 
-const 	request = require('request'),
-		config  = require('config'),
-		async 	= require('async'),
-		utils 	= require('../utils'),
-		logger 	= require('../logger'),
-		hooks 	= require('./hooks');
+const request = require("request");
+const config = require("config");
+const async = require("async");
+
+const hooks = require('./hooks');
+const moduleManager = require("../../index");
+
+const utils = moduleManager.modules["utils"];
+const logger = moduleManager.modules["logger"];
 
 module.exports = {
 
@@ -34,11 +37,11 @@ module.exports = {
 			(res, body, next) => {
 				next(null, JSON.parse(body));
 			}
-		], (err, data) => {
+		], async (err, data) => {
 			console.log(data.error);
 			if (err || data.error) {
 				if (!err) err = data.error.message;
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("APIS_SEARCH_YOUTUBE", `Searching youtube failed with query "${query}". "${err}"`);
 				return cb({status: 'failure', message: err});
 			}

+ 9 - 6
backend/logic/actions/hooks/adminRequired.js

@@ -1,9 +1,12 @@
-const cache = require('../../cache');
-const db = require('../../db');
-const utils = require('../../utils');
-const logger = require('../../logger');
 const async = require('async');
 
+const moduleManager = require("../../../index");
+
+const db = moduleManager.modules["db"];
+const cache = moduleManager.modules["cache"];
+const utils = moduleManager.modules["utils"];
+const logger = moduleManager.modules["logger"];
+
 module.exports = function(next) {
 	return function(session) {
 		let args = [];
@@ -23,9 +26,9 @@ module.exports = function(next) {
 				if (user.role !== 'admin') return next('Insufficient permissions.');
 				next();
 			}
-		], (err) => {
+		], async (err) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.info("ADMIN_REQUIRED", `User failed to pass admin required check. "${err}"`);
 				return cb({status: 'failure', message: err});
 			}

+ 8 - 5
backend/logic/actions/hooks/loginRequired.js

@@ -1,8 +1,11 @@
-const cache = require('../../cache');
-const utils = require('../../utils');
-const logger = require('../../logger');
 const async = require('async');
 
+const moduleManager = require("../../../index");
+
+const cache = moduleManager.modules["cache"];
+const utils = moduleManager.modules["utils"];
+const logger = moduleManager.modules["logger"];
+
 module.exports = function(next) {
 	return function(session) {
 		let args = [];
@@ -17,9 +20,9 @@ module.exports = function(next) {
 				this.session = session;
 				next();
 			}
-		], (err) => {
+		], async (err) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.info("LOGIN_REQUIRED", `User failed to pass login required check.`);
 				return cb({status: 'failure', message: err});
 			}

+ 10 - 7
backend/logic/actions/hooks/ownerRequired.js

@@ -1,9 +1,12 @@
-const cache = require('../../cache');
-const db = require('../../db');
-const utils = require('../../utils');
-const logger = require('../../logger');
 const async = require('async');
-const stations = require('../../stations');
+
+const moduleManager = require("../../../index");
+
+const db = moduleManager.modules["db"];
+const cache = moduleManager.modules["cache"];
+const utils = moduleManager.modules["utils"];
+const logger = moduleManager.modules["logger"];
+const stations = moduleManager.modules["stations"];
 
 module.exports = function(next) {
 	return function(session, stationId) {
@@ -29,9 +32,9 @@ module.exports = function(next) {
 				if (station.type === 'community' && station.owner === session.userId) return next(true);
 				next('Invalid permissions.');
 			}
-		], (err) => {
+		], async (err) => {
 			if (err !== true) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.info("OWNER_REQUIRED", `User failed to pass owner required check for station "${stationId}". "${err}"`);
 				return cb({status: 'failure', message: err});
 			}

+ 16 - 14
backend/logic/actions/news.js

@@ -2,11 +2,13 @@
 
 const async = require('async');
 
-const db = require('../db');
-const cache = require('../cache');
-const utils = require('../utils');
-const logger = require('../logger');
 const hooks = require('./hooks');
+const moduleManager = require("../../index");
+
+const db = moduleManager.modules["db"];
+const cache = moduleManager.modules["cache"];
+const utils = moduleManager.modules["utils"];
+const logger = moduleManager.modules["logger"];
 
 cache.sub('news.create', news => {
 	utils.socketsFromUser(news.createdBy, sockets => {
@@ -45,9 +47,9 @@ module.exports = {
 			(next) => {
 				db.models.news.find({}).sort({ createdAt: 'desc' }).exec(next);
 			}
-		], (err, news) => {
+		], async (err, news) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("NEWS_INDEX", `Indexing news failed. "${err}"`);
 				return cb({status: 'failure', message: err});
 			}
@@ -71,9 +73,9 @@ module.exports = {
 				data.createdAt = Date.now();
 				db.models.news.create(data, next);
 			}
-		], (err, news) => {
+		], async (err, news) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("NEWS_CREATE", `Creating news failed. "${err}"`);
 				return cb({ 'status': 'failure', 'message': err });
 			}
@@ -94,9 +96,9 @@ module.exports = {
 			(next) => {
 				db.models.news.findOne({}).sort({ createdAt: 'desc' }).exec(next);
 			}
-		], (err, news) => {
+		], async (err, news) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("NEWS_NEWEST", `Getting the latest news failed. "${err}"`);
 				return cb({ 'status': 'failure', 'message': err });
 			}
@@ -115,9 +117,9 @@ module.exports = {
 	//TODO Pass in an id, not an object
 	//TODO Fix this
 	remove: hooks.adminRequired((session, news, cb, userId) => {
-		db.models.news.deleteOne({ _id: news._id }, err => {
+		db.models.news.deleteOne({ _id: news._id }, async err => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("NEWS_REMOVE", `Removing news "${news._id}" failed for user "${userId}". "${err}"`);
 				return cb({ 'status': 'failure', 'message': err });
 			} else {
@@ -138,9 +140,9 @@ module.exports = {
 	 */
 	//TODO Fix this
 	update: hooks.adminRequired((session, _id, news, cb, userId) => {
-		db.models.news.updateOne({ _id }, news, { upsert: true }, err => {
+		db.models.news.updateOne({ _id }, news, { upsert: true }, async err => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("NEWS_UPDATE", `Updating news "${_id}" failed for user "${userId}". "${err}"`);
 				return cb({ 'status': 'failure', 'message': err });
 			} else {

+ 34 - 32
backend/logic/actions/playlists.js

@@ -1,14 +1,16 @@
 'use strict';
 
-const db = require('../db');
-const io = require('../io');
-const cache = require('../cache');
-const utils = require('../utils');
-const logger = require('../logger');
-const hooks = require('./hooks');
 const async = require('async');
-const playlists = require('../playlists');
-const songs = require('../songs');
+
+const hooks = require('./hooks');
+const moduleManager = require("../../index");
+
+const db = moduleManager.modules["db"];
+const cache = moduleManager.modules["cache"];
+const utils = moduleManager.modules["utils"];
+const logger = moduleManager.modules["logger"];
+const playlists = moduleManager.modules["playlists"];
+const songs = moduleManager.modules["songs"];
 
 cache.sub('playlist.create', playlistId => {
 	playlists.getPlaylist(playlistId, (err, playlist) => {
@@ -90,9 +92,9 @@ let lib = {
 				if (!playlist || playlist.createdBy !== userId) return next('Playlist not found.');
 				next(null, playlist.songs[0]);
 			}
-		], (err, song) => {
+		], async (err, song) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("PLAYLIST_GET_FIRST_SONG", `Getting the first song of playlist "${playlistId}" failed for user "${userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 			}
@@ -116,9 +118,9 @@ let lib = {
 			(next) => {
 				db.models.playlist.find({ createdBy: userId }, next);
 			}
-		], (err, playlists) => {
+		], async (err, playlists) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("PLAYLIST_INDEX_FOR_USER", `Indexing playlists for user "${userId}" failed. "${err}"`);
 				return cb({ status: 'failure', message: err});
 			}
@@ -155,9 +157,9 @@ let lib = {
 				}, next);
 			}
 
-		], (err, playlist) => {
+		], async (err, playlist) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("PLAYLIST_CREATE", `Creating private playlist failed for user "${userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 			}
@@ -187,9 +189,9 @@ let lib = {
 				if (!playlist || playlist.createdBy !== userId) return next('Playlist not found');
 				next(null, playlist);
 			}
-		], (err, playlist) => {
+		], async (err, playlist) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("PLAYLIST_GET", `Getting private playlist "${playlistId}" failed for user "${userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 			}
@@ -220,9 +222,9 @@ let lib = {
 			(res, next) => {
 				playlists.updatePlaylist(playlistId, next)
 			}
-		], (err, playlist) => {
+		], async (err, playlist) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("PLAYLIST_UPDATE", `Updating private playlist "${playlistId}" failed for user "${userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 			}
@@ -280,9 +282,9 @@ let lib = {
 				});
 			}
 		],
-		(err, playlist, newSong) => {
+		async (err, playlist, newSong) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("PLAYLIST_ADD_SONG", `Adding song "${songId}" to private playlist "${playlistId}" failed for user "${userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 			} else {
@@ -329,9 +331,9 @@ let lib = {
 				if (!playlist || playlist.createdBy !== userId) return next('Playlist not found.');
 				next(null, playlist);
 			}
-		], (err, playlist) => {
+		], async (err, playlist) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("PLAYLIST_IMPORT", `Importing a YouTube playlist to private playlist "${playlistId}" failed for user "${userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 			} else {
@@ -370,9 +372,9 @@ let lib = {
 			(res, next) => {
 				playlists.updatePlaylist(playlistId, next);
 			}
-		], (err, playlist) => {
+		], async (err, playlist) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("PLAYLIST_REMOVE_SONG", `Removing song "${songId}" from private playlist "${playlistId}" failed for user "${userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 			} else {
@@ -400,9 +402,9 @@ let lib = {
 			(res, next) => {
 				playlists.updatePlaylist(playlistId, next);
 			}
-		], (err, playlist) => {
+		], async (err, playlist) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("PLAYLIST_UPDATE_DISPLAY_NAME", `Updating display name to "${displayName}" for private playlist "${playlistId}" failed for user "${userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 			}
@@ -459,9 +461,9 @@ let lib = {
 			(res, next) => {
 				playlists.updatePlaylist(playlistId, next);
 			}
-		], (err, playlist) => {
+		], async (err, playlist) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("PLAYLIST_MOVE_SONG_TO_TOP", `Moving song "${songId}" to the top for private playlist "${playlistId}" failed for user "${userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 			}
@@ -515,9 +517,9 @@ let lib = {
 			(res, next) => {
 				playlists.updatePlaylist(playlistId, next);
 			}
-		], (err, playlist) => {
+		], async (err, playlist) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("PLAYLIST_MOVE_SONG_TO_BOTTOM", `Moving song "${songId}" to the bottom for private playlist "${playlistId}" failed for user "${userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 			}
@@ -540,9 +542,9 @@ let lib = {
 			(next) => {
 				playlists.deletePlaylist(playlistId, next);
 			}
-		], (err) => {
+		], async (err) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("PLAYLIST_REMOVE", `Removing private playlist "${playlistId}" failed for user "${userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 			}

+ 14 - 11
backend/logic/actions/punishments.js

@@ -1,12 +1,15 @@
 'use strict';
 
-const 	hooks 	    = require('./hooks'),
-	 	async 	    = require('async'),
-	 	logger 	    = require('../logger'),
-	 	utils 	    = require('../utils'),
-		cache       = require('../cache'),
-	 	db 	        = require('../db'),
-		punishments = require('../punishments');
+const async = require('async');
+
+const hooks = require('./hooks');
+const moduleManager = require("../../index");
+
+const logger = moduleManager.modules["logger"];
+const utils = moduleManager.modules["utils"];
+const cache = moduleManager.modules["cache"];
+const db = moduleManager.modules["db"];
+const punishments = moduleManager.modules["punishments"];
 
 cache.sub('ip.ban', data => {
 	utils.socketsFromIP(data.ip, sockets => {
@@ -30,9 +33,9 @@ module.exports = {
 			(next) => {
 				db.models.punishment.find({}, next);
 			}
-		], (err, punishments) => {
+		], async (err, punishments) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("PUNISHMENTS_INDEX", `Indexing punishments failed. "${err}"`);
 				return cb({ 'status': 'failure', 'message': err});
 			}
@@ -105,9 +108,9 @@ module.exports = {
 				cache.pub('ip.ban', {ip: value, punishment});
 				next();
 			},
-		], (err) => {
+		], async (err) => {
 			if (err && err !== true) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("BAN_IP", `User ${userId} failed to ban IP address ${value} with the reason ${reason}. '${err}'`);
 				cb({ status: 'failure', message: err });
 			} else {

+ 21 - 18
backend/logic/actions/queueSongs.js

@@ -1,17 +1,20 @@
 'use strict';
 
-const db = require('../db');
-const utils = require('../utils');
-const logger = require('../logger');
-const notifications = require('../notifications');
-const cache = require('../cache');
-const async = require('async');
 const config = require('config');
+const async = require('async');
 const request = require('request');
+
 const hooks = require('./hooks');
 
+const moduleManager = require("../../index");
+
+const db = moduleManager.modules["db"];
+const utils = moduleManager.modules["utils"];
+const logger = moduleManager.modules["logger"];
+const cache = moduleManager.modules["cache"];
+
 cache.sub('queue.newSong', songId => {
-	db.models.queueSong.findOne({songId}, (err, song) => {
+	db.models.queueSong.findOne({_id: songId}, (err, song) => {
 		utils.emitToRoom('admin.queue', 'event:admin.queueSong.added', song);
 	});
 });
@@ -21,7 +24,7 @@ cache.sub('queue.removedSong', songId => {
 });
 
 cache.sub('queue.update', songId => {
-	db.models.queueSong.findOne({songId}, (err, song) => {
+	db.models.queueSong.findOne({_id: songId}, (err, song) => {
 		utils.emitToRoom('admin.queue', 'event:admin.queueSong.updated', song);
 	});
 });
@@ -39,9 +42,9 @@ let lib = {
 			(next) => {
 				db.models.queueSong.find({}, next);
 			}
-		], (err, songs) => {
+		], async (err, songs) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("QUEUE_INDEX", `Indexing queuesongs failed. "${err}"`);
 				return cb({status: 'failure', message: err});
 			} else {
@@ -93,9 +96,9 @@ let lib = {
 				if (!updated) return next('No properties changed');
 				db.models.queueSong.updateOne({_id: songId}, {$set}, {runValidators: true}, next);
 			}
-		], (err) => {
+		], async (err) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await  utils.getError(err);
 				logger.error("QUEUE_UPDATE", `Updating queuesong "${songId}" failed for user ${userId}. "${err}"`);
 				return cb({status: 'failure', message: err});
 			}
@@ -118,9 +121,9 @@ let lib = {
 			(next) => {
 				db.models.queueSong.deleteOne({_id: songId}, next);
 			}
-		], (err) => {
+		], async (err) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("QUEUE_REMOVE", `Removing queuesong "${songId}" failed for user ${userId}. "${err}"`);
 				return cb({status: 'failure', message: err});
 			}
@@ -191,9 +194,9 @@ let lib = {
 					}
 				});
 			}
-		], (err, newSong) => {
+		], async (err, newSong) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("QUEUE_ADD", `Adding queuesong "${songId}" failed for user ${userId}. "${err}"`);
 				return cb({status: 'failure', message: err});
 			}
@@ -230,9 +233,9 @@ let lib = {
 					});
 				}
 			}
-		], (err) => {
+		], async (err) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("QUEUE_IMPORT", `Importing a YouTube playlist to the queue failed for user "${userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 			} else {

+ 19 - 15
backend/logic/actions/reports.js

@@ -2,12 +2,16 @@
 
 const async = require('async');
 
-const db = require('../db');
-const cache = require('../cache');
-const utils = require('../utils');
-const logger = require('../logger');
 const hooks = require('./hooks');
-const songs = require('../songs');
+
+const moduleManager = require("../../index");
+
+const db = moduleManager.modules["db"];
+const cache = moduleManager.modules["cache"];
+const utils = moduleManager.modules["utils"];
+const logger = moduleManager.modules["logger"];
+const songs = moduleManager.modules["songs"];
+
 const reportableIssues = [
 	{
 		name: 'Video',
@@ -71,9 +75,9 @@ module.exports = {
 			(next) => {
 				db.models.report.find({ resolved: false }).sort({ released: 'desc' }).exec(next);
 			}
-		], (err, reports) => {
+		], async (err, reports) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("REPORTS_INDEX", `Indexing reports failed. "${err}"`);
 				return cb({ 'status': 'failure', 'message': err});
 			}
@@ -94,9 +98,9 @@ module.exports = {
 			(next) => {
 				db.models.report.findOne({ _id: reportId }).exec(next);
 			}
-		], (err, report) => {
+		], async (err, report) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("REPORTS_FIND_ONE", `Finding report "${reportId}" failed. "${err}"`);
 				return cb({ 'status': 'failure', 'message': err });
 			}
@@ -125,9 +129,9 @@ module.exports = {
 				}
 				next(null, data);
 			}
-		], (err, data) => {
+		], async (err, data) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("GET_REPORTS_FOR_SONG", `Indexing reports for song "${songId}" failed. "${err}"`);
 				return cb({ 'status': 'failure', 'message': err});
 			} else {
@@ -159,9 +163,9 @@ module.exports = {
 					else next();
 				});
 			}
-		], (err) => {
+		], async (err) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await  utils.getError(err);
 				logger.error("REPORTS_RESOLVE", `Resolving report "${reportId}" failed by user "${userId}". "${err}"`);
 				return cb({ 'status': 'failure', 'message': err});
 			} else {
@@ -227,9 +231,9 @@ module.exports = {
 				db.models.report.create(data, next);
 			}
 
-		], (err, report) => {
+		], async (err, report) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("REPORTS_CREATE", `Creating report for "${data.songId}" failed by user "${userId}". "${err}"`);
 				return cb({ 'status': 'failure', 'message': err });
 			} else {

+ 33 - 30
backend/logic/actions/songs.js

@@ -1,27 +1,30 @@
 'use strict';
 
-const db = require('../db');
-const io = require('../io');
-const songs = require('../songs');
-const cache = require('../cache');
 const async = require('async');
-const utils = require('../utils');
-const logger = require('../logger');
+
 const hooks = require('./hooks');
 const queueSongs = require('./queueSongs');
 
+const moduleManager = require("../../index");
+
+const db = moduleManager.modules["db"];
+const songs = moduleManager.modules["songs"];
+const cache = moduleManager.modules["cache"];
+const utils = moduleManager.modules["utils"];
+const logger = moduleManager.modules["logger"];
+
 cache.sub('song.removed', songId => {
 	utils.emitToRoom('admin.songs', 'event:admin.song.removed', songId);
 });
 
 cache.sub('song.added', songId => {
-	db.models.song.findOne({songId}, (err, song) => {
+	db.models.song.findOne({_id: songId}, (err, song) => {
 		utils.emitToRoom('admin.songs', 'event:admin.song.added', song);
 	});
 });
 
 cache.sub('song.updated', songId => {
-	db.models.song.findOne({songId}, (err, song) => {
+	db.models.song.findOne({_id: songId}, (err, song) => {
 		utils.emitToRoom('admin.songs', 'event:admin.song.updated', song);
 	});
 });
@@ -75,9 +78,9 @@ module.exports = {
 			(next) => {
 				db.models.song.countDocuments({}, next);
 			}
-		], (err, count) => {
+		], async (err, count) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("SONGS_LENGTH", `Failed to get length from songs. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -98,9 +101,9 @@ module.exports = {
 			(next) => {
 				db.models.song.find({}).limit(15 * set).exec(next);
 			}
-		], (err, songs) => {
+		], async (err, songs) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("SONGS_GET_SET", `Failed to get set from songs. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -125,9 +128,9 @@ module.exports = {
 			(next) => {
 				db.models.song.findOne({ songId }).exec(next);
 			}
-		], (err, song) => {
+		], async (err, song) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("SONGS_GET_SONG", `Failed to get song ${songId}. "${err}"`);
 				return cb({ status: 'failure', message: err });
 			} else {
@@ -154,9 +157,9 @@ module.exports = {
 			(res, next) => {
 				songs.updateSong(songId, next);
 			}
-		], (err, song) => {
+		], async (err, song) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("SONGS_UPDATE", `Failed to update song "${songId}". "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -182,9 +185,9 @@ module.exports = {
 			(res, next) => {//TODO Check if res gets returned from above
 				cache.hdel('songs', songId, next);
 			}
-		], (err) => {
+		], async (err) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("SONGS_UPDATE", `Failed to remove song "${songId}". "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -225,9 +228,9 @@ module.exports = {
 					next();
 				});
 			},
-		], (err) => {
+		], async (err) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("SONGS_ADD", `User "${userId}" failed to add song. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -256,9 +259,9 @@ module.exports = {
 				if (!song) return next('No song found with that id.');
 				next(null, song);
 			}
-		], (err, song) => {
+		], async (err, song) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("SONGS_LIKE", `User "${userId}" failed to like song ${songId}. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -304,9 +307,9 @@ module.exports = {
 				if (!song) return next('No song found with that id.');
 				next(null, song);
 			}
-		], (err, song) => {
+		], async (err, song) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("SONGS_DISLIKE", `User "${userId}" failed to like song ${songId}. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -352,9 +355,9 @@ module.exports = {
 				if (!song) return next('No song found with that id.');
 				next(null, song);
 			}
-		], (err, song) => {
+		], async (err, song) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("SONGS_UNDISLIKE", `User "${userId}" failed to like song ${songId}. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -421,9 +424,9 @@ module.exports = {
 				if (!song) return next('No song found with that id.');
 				next(null, song);
 			}
-		], (err, song) => {
+		], async (err, song) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("SONGS_UNLIKE", `User "${userId}" failed to like song ${songId}. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -469,9 +472,9 @@ module.exports = {
 				if (!song) return next('No song found with that id.');
 				next(null, song);
 			}
-		], (err, song) => {
+		], async (err, song) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("SONGS_GET_OWN_RATINGS", `User "${userId}" failed to get ratings for ${songId}. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}

+ 94 - 90
backend/logic/actions/stations.js

@@ -5,15 +5,18 @@ const async   = require('async'),
 	  config  = require('config'),
 	  _		  =  require('underscore')._;
 
-const io = require('../io');
-const db = require('../db');
-const cache = require('../cache');
-const notifications = require('../notifications');
-const utils = require('../utils');
-const logger = require('../logger');
-const stations = require('../stations');
-const songs = require('../songs');
 const hooks = require('./hooks');
+
+const moduleManager = require("../../index");
+
+const db = moduleManager.modules["db"];
+const cache = moduleManager.modules["cache"];
+const notifications = moduleManager.modules["notifications"];
+const utils = moduleManager.modules["utils"];
+const logger = moduleManager.modules["logger"];
+const stations = moduleManager.modules["stations"];
+const songs = moduleManager.modules["songs"];
+
 let userList = {};
 let usersPerStation = {};
 let usersPerStationCount = {};
@@ -29,39 +32,40 @@ setInterval(() => {
 	usersPerStationCount = {};
 
 	async.each(Object.keys(userList), function(socketId, next) {
-		let socket = utils.socketFromSession(socketId);
-		let stationId = userList[socketId];
-		if (!socket || Object.keys(socket.rooms).indexOf(`station.${stationId}`) === -1) {
-			if (stationsCountUpdated.indexOf(stationId) === -1) stationsCountUpdated.push(stationId);
-			if (stationsUpdated.indexOf(stationId) === -1) stationsUpdated.push(stationId);
-			delete userList[socketId];
-			return next();
-		}
-		if (!usersPerStationCount[stationId]) usersPerStationCount[stationId] = 0;
-		usersPerStationCount[stationId]++;
-		if (!usersPerStation[stationId]) usersPerStation[stationId] = [];
-
-		async.waterfall([
-			(next) => {
-				if (!socket.session || !socket.session.sessionId) return next('No session found.');
-				cache.hget('sessions', socket.session.sessionId, next);
-			},
-
-			(session, next) => {
-				if (!session) return next('Session not found.');
-				db.models.user.findOne({_id: session.userId}, next);
-			},
-
-			(user, next) => {
-				if (!user) return next('User not found.');
-				if (usersPerStation[stationId].indexOf(user.username) !== -1) return next('User already in the list.');
-				next(null, user.username);
-			}
-		], (err, username) => {
-			if (!err) {
-				usersPerStation[stationId].push(username);
+		utils.socketFromSession(socketId).then((socket) => {
+			let stationId = userList[socketId];
+			if (!socket || Object.keys(socket.rooms).indexOf(`station.${stationId}`) === -1) {
+				if (stationsCountUpdated.indexOf(stationId) === -1) stationsCountUpdated.push(stationId);
+				if (stationsUpdated.indexOf(stationId) === -1) stationsUpdated.push(stationId);
+				delete userList[socketId];
+				return next();
 			}
-			next();
+			if (!usersPerStationCount[stationId]) usersPerStationCount[stationId] = 0;
+			usersPerStationCount[stationId]++;
+			if (!usersPerStation[stationId]) usersPerStation[stationId] = [];
+
+			async.waterfall([
+				(next) => {
+					if (!socket.session || !socket.session.sessionId) return next('No session found.');
+					cache.hget('sessions', socket.session.sessionId, next);
+				},
+
+				(session, next) => {
+					if (!session) return next('Session not found.');
+					db.models.user.findOne({_id: session.userId}, next);
+				},
+
+				(user, next) => {
+					if (!user) return next('User not found.');
+					if (usersPerStation[stationId].indexOf(user.username) !== -1) return next('User already in the list.');
+					next(null, user.username);
+				}
+			], (err, username) => {
+				if (!err) {
+					usersPerStation[stationId].push(username);
+				}
+				next();
+			});
 		});
 		//TODO Code to show users
 	}, (err) => {
@@ -99,10 +103,10 @@ cache.sub('station.updateUsers', stationId => {
 cache.sub('station.updateUserCount', stationId => {
 	let count = usersPerStationCount[stationId] || 0;
 	utils.emitToRoom(`station.${stationId}`, "event:userCount.updated", count);
-	stations.getStation(stationId, (err, station) => {
+	stations.getStation(stationId, async (err, station) => {
 		if (station.privacy === 'public') utils.emitToRoom('home', "event:userCount.updated", stationId, count);
 		else {
-			let sockets = utils.getRoomSockets('home');
+			let sockets = await utils.getRoomSockets('home');
 			for (let socketId in sockets) {
 				let socket = sockets[socketId];
 				let session = sockets[socketId].session;
@@ -161,14 +165,14 @@ cache.sub('station.remove', stationId => {
 });
 
 cache.sub('station.create', stationId => {
-	stations.initializeStation(stationId, (err, station) => {
+	stations.initializeStation(stationId, async (err, station) => {
 		station.userCount = usersPerStationCount[stationId] || 0;
 		if (err) console.error(err);
 		utils.emitToRoom('admin.stations', 'event:admin.station.added', station);
 		// TODO If community, check if on whitelist
 		if (station.privacy === 'public') utils.emitToRoom('home', "event:stations.created", station);
 		else {
-			let sockets = utils.getRoomSockets('home');
+			let sockets = await utils.getRoomSockets('home');
 			for (let socketId in sockets) {
 				let socket = sockets[socketId];
 				let session = sockets[socketId].session;
@@ -241,9 +245,9 @@ module.exports = {
 					next(null, resultStations);
 				});
 			}
-		], (err, stations) => {
+		], async (err, stations) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("STATIONS_INDEX", `Indexing stations failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -269,9 +273,9 @@ module.exports = {
 				if (!station) return next('Station not found.');
 				next(null, station);
 			}
-		], (err, station) => {
+		], async (err, station) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("STATIONS_FIND_BY_NAME", `Finding station "${stationName}" failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -307,9 +311,9 @@ module.exports = {
 				if (!playlist) return next('Playlist not found.');
 				next(null, playlist);
 			}
-		], (err, playlist) => {
+		], async (err, playlist) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("STATIONS_GET_PLAYLIST", `Getting playlist for station "${stationId}" failed. "${err}"`);
 				return cb({ status: 'failure', message: err });
 			} else {
@@ -398,9 +402,9 @@ module.exports = {
 					next(null, data);
 				});
 			}
-		], (err, data) => {
+		], async (err, data) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("STATIONS_JOIN", `Joining station "${stationName}" failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -429,9 +433,9 @@ module.exports = {
 			(res, next) => {
 				stations.updateStation(stationId, next);
 			}
-		], (err, station) => {
+		], async (err, station) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("STATIONS_UPDATE_LOCKED_STATUS", `Toggling the queue lock for station "${stationId}" failed. "${err}"`);
 				return cb({ status: 'failure', message: err });
 			} else {
@@ -482,9 +486,9 @@ module.exports = {
 				if (!station) return next('Station not found.');
 				next(null, station);
 			}
-		], (err, station) => {
+		], async (err, station) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("STATIONS_VOTE_SKIP", `Vote skipping station "${stationId}" failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -512,9 +516,9 @@ module.exports = {
 				if (!station) return next('Station not found.');
 				next();
 			}
-		], (err) => {
+		], async (err) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("STATIONS_FORCE_SKIP", `Force skipping station "${stationId}" failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -543,9 +547,9 @@ module.exports = {
 				if (!station) return next('Station not found.');
 				next();
 			}
-		], (err, userCount) => {
+		], async (err, userCount) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("STATIONS_LEAVE", `Leaving station "${stationId}" failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -573,9 +577,9 @@ module.exports = {
 			(res, next) => {
 				stations.updateStation(stationId, next);
 			}
-		], (err) => {
+		], async (err) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("STATIONS_UPDATE_DISPLAY_NAME", `Updating station "${stationId}" displayName to "${newName}" failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -601,9 +605,9 @@ module.exports = {
 			(res, next) => {
 				stations.updateStation(stationId, next);
 			}
-		], (err) => {
+		], async (err) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("STATIONS_UPDATE_DISPLAY_NAME", `Updating station "${stationId}" displayName to "${newDisplayName}" failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -629,9 +633,9 @@ module.exports = {
 			(res, next) => {
 				stations.updateStation(stationId, next);
 			}
-		], (err) => {
+		], async (err) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("STATIONS_UPDATE_DESCRIPTION", `Updating station "${stationId}" description to "${newDescription}" failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -657,9 +661,9 @@ module.exports = {
 			(res, next) => {
 				stations.updateStation(stationId, next);
 			}
-		], (err) => {
+		], async (err) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("STATIONS_UPDATE_PRIVACY", `Updating station "${stationId}" privacy to "${newPrivacy}" failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -685,9 +689,9 @@ module.exports = {
 			(res, next) => {
 				stations.updateStation(stationId, next);
 			}
-		], (err) => {
+		], async (err) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("STATIONS_UPDATE_GENRES", `Updating station "${stationId}" genres to "${newGenres}" failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -713,9 +717,9 @@ module.exports = {
 			(res, next) => {
 				stations.updateStation(stationId, next);
 			}
-		], (err) => {
+		], async (err) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("STATIONS_UPDATE_BLACKLISTED_GENRES", `Updating station "${stationId}" blacklisted genres to "${newBlacklistedGenres}" failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -747,9 +751,9 @@ module.exports = {
 			(res, next) => {
 				stations.updateStation(stationId, next);
 			}
-		], (err) => {
+		], async (err) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("STATIONS_UPDATE_PARTY_MODE", `Updating station "${stationId}" party mode to "${newPartyMode}" failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -782,9 +786,9 @@ module.exports = {
 			(res, next) => {
 				stations.updateStation(stationId, next);
 			}
-		], (err) => {
+		], async (err) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("STATIONS_PAUSE", `Pausing station "${stationId}" failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -818,9 +822,9 @@ module.exports = {
 			(res, next) => {
 				stations.updateStation(stationId, next);
 			}
-		], (err) => {
+		], async (err) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("STATIONS_RESUME", `Resuming station "${stationId}" failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -846,9 +850,9 @@ module.exports = {
 			(next) => {
 				cache.hdel('stations', stationId, err => next(err));
 			}
-		], (err) => {
+		], async (err) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("STATIONS_REMOVE", `Removing station "${stationId}" failed. "${err}"`);
 				return cb({ 'status': 'failure', 'message': err });
 			}
@@ -913,9 +917,9 @@ module.exports = {
 					}, next);
 				}
 			}
-		], (err, station) => {
+		], async (err, station) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("STATIONS_CREATE", `Creating station failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -1037,9 +1041,9 @@ module.exports = {
 			(res, next) => {
 				stations.updateStation(stationId, next);
 			}
-		], (err, station) => {
+		], async (err, station) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("STATIONS_ADD_SONG_TO_QUEUE", `Adding song "${songId}" to station "${stationId}" queue failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -1084,9 +1088,9 @@ module.exports = {
 			(res, next) => {
 				stations.updateStation(stationId, next);
 			}
-		], (err, station) => {
+		], async (err, station) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("STATIONS_REMOVE_SONG_TO_QUEUE", `Removing song "${songId}" from station "${stationId}" queue failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -1121,9 +1125,9 @@ module.exports = {
 					return next('Insufficient permissions.');
 				});
 			}
-		], (err, station) => {
+		], async (err, station) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("STATIONS_GET_QUEUE", `Getting queue for station "${stationId}" failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}
@@ -1163,9 +1167,9 @@ module.exports = {
 			(res, next) => {
 				stations.updateStation(stationId, next);
 			}
-		], (err, station) => {
+		], async (err, station) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("STATIONS_SELECT_PRIVATE_PLAYLIST", `Selecting private playlist "${playlistId}" for station "${stationId}" failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 			}

+ 88 - 65
backend/logic/actions/users.js

@@ -4,15 +4,18 @@ const async = require('async');
 const config = require('config');
 const request = require('request');
 const bcrypt = require('bcrypt');
+const sha256 = require('sha256');
 
-const db = require('../db');
-const mail = require('../mail');
-const cache = require('../cache');
-const punishments = require('../punishments');
-const utils = require('../utils');
 const hooks = require('./hooks');
-const sha256 = require('sha256');
-const logger = require('../logger');
+
+const moduleManager = require("../../index");
+
+const db = moduleManager.modules["db"];
+const mail = moduleManager.modules["mail"];
+const cache = moduleManager.modules["cache"];
+const punishments = moduleManager.modules["punishments"];
+const utils = moduleManager.modules["utils"];
+const logger = moduleManager.modules["logger"];
 
 cache.sub('user.updateUsername', user => {
 	utils.socketsFromUser(user._id, sockets => {
@@ -84,9 +87,9 @@ module.exports = {
 			(next) => {
 				db.models.user.find({}).exec(next);
 			}
-		], (err, users) => {
+		], async (err, users) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("USER_INDEX", `Indexing users failed. "${err}"`);
 				return cb({status: 'failure', message: err});
 			} else {
@@ -147,16 +150,21 @@ module.exports = {
 			},
 
 			(user, next) => {
-				let sessionId = utils.guid();
+				utils.guid().then((sessionId) => {
+					next(null, user, sessionId);
+				});
+			},
+
+			(user, sessionId, next) => {
 				cache.hset('sessions', sessionId, cache.schemas.session(sessionId, user._id), (err) => {
 					if (err) return next(err);
 					next(null, sessionId);
 				});
 			}
 
-		], (err, sessionId) => {
+		], async (err, sessionId) => {
 			if (err && err !== true) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("USER_PASSWORD_LOGIN", `Login failed with password for user "${identifier}". "${err}"`);
 				return cb({status: 'failure', message: err});
 			}
@@ -176,9 +184,9 @@ module.exports = {
 	 * @param {Object} recaptcha - the recaptcha data
 	 * @param {Function} cb - gets called with the result
 	 */
-	register: function(session, username, email, password, recaptcha, cb) {
+	register: async function(session, username, email, password, recaptcha, cb) {
 		email = email.toLowerCase();
-		let verificationToken = utils.generateRandomString(64);
+		let verificationToken = await utils.generateRandomString(64);
 		async.waterfall([
 
 			// verify the request with google recaptcha
@@ -225,10 +233,16 @@ module.exports = {
 				bcrypt.hash(sha256(password), salt, next)
 			},
 
-			// save the new user to the database
 			(hash, next) => {
+				utils.generateRandomString(12).then((_id) => {
+					next(null, hash, _id);
+				});
+			},
+
+			// save the new user to the database
+			(hash, _id, next) => {
 				db.models.user.create({
-					_id: utils.generateRandomString(12),//TODO Check if exists
+					_id,
 					username,
 					email: {
 						address: email,
@@ -250,9 +264,9 @@ module.exports = {
 				});
 			}
 
-		], (err) => {
+		], async (err) => {
 			if (err && err !== true) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("USER_PASSWORD_REGISTER", `Register failed with password for user "${username}"."${err}"`);
 				cb({status: 'failure', message: err});
 			} else {
@@ -290,9 +304,9 @@ module.exports = {
 			(session, next) => {
 				cache.hdel('sessions', session.sessionId, next);
 			}
-		], (err) => {
+		], async (err) => {
 			if (err && err !== true) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("USER_LOGOUT", `Logout failed. "${err}" `);
 				cb({ status: 'failure', message: err });
 			} else {
@@ -349,9 +363,9 @@ module.exports = {
 				});
 			}
 
-		], err => {
+		], async err => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("REMOVE_SESSIONS_FOR_USER", `Couldn't remove all sessions for user "${userId}". "${err}"`);
 				return cb({ status: 'failure', message: err });
 			} else {
@@ -379,9 +393,9 @@ module.exports = {
 				if (!account) return next('User not found.');
 				next(null, account);
 			}
-		], (err, account) => {
+		], async (err, account) => {
 			if (err && err !== true) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("FIND_BY_USERNAME", `User not found for username "${username}". "${err}"`);
 				cb({status: 'failure', message: err});
 			} else {
@@ -413,14 +427,23 @@ module.exports = {
 	 */
 	getUsernameFromId: (session, userId, cb) => {
 		db.models.user.findById(userId).then(user => {
-			logger.success("GET_USERNAME_FROM_ID", `Found username for userId "${userId}".`);
-			return cb({
-				status: 'success',
-				data: user.username
-			});
-		}).catch(err => {
+			if (user) {
+				logger.success("GET_USERNAME_FROM_ID", `Found username for userId "${userId}".`);
+				return cb({
+					status: 'success',
+					data: user.username
+				});
+			} else {
+				logger.error("GET_USERNAME_FROM_ID", `Getting the username from userId "${userId}" failed. User not found.`);
+				cb({
+					status: 'failure',
+					message: "Couldn't find the user."
+				});
+			}
+			
+		}).catch(async err => {
 			if (err && err !== true) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("GET_USERNAME_FROM_ID", `Getting the username from userId "${userId}" failed. "${err}"`);
 				cb({ status: 'failure', message: err });
 			}
@@ -453,9 +476,9 @@ module.exports = {
 				if (!user) return next('User not found.');
 				next(null, user);
 			}
-		], (err, user) => {
+		], async (err, user) => {
 			if (err && err !== true) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("FIND_BY_SESSION", `User not found. "${err}"`);
 				cb({status: 'failure', message: err});
 			} else {
@@ -516,9 +539,9 @@ module.exports = {
 			(next) => {
 				db.models.user.updateOne({ _id: updatingUserId }, {$set: {username: newUsername}}, {runValidators: true}, next);
 			}
-		], (err) => {
+		], async (err) => {
 			if (err && err !== true) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("UPDATE_USERNAME", `Couldn't update username for user "${updatingUserId}" to username "${newUsername}". "${err}"`);
 				cb({status: 'failure', message: err});
 			} else {
@@ -541,9 +564,9 @@ module.exports = {
 	 * @param {Function} cb - gets called with the result
 	 * @param {String} userId - the userId automatically added by hooks
 	 */
-	updateEmail: hooks.loginRequired((session, updatingUserId, newEmail, cb, userId) => {
+	updateEmail: hooks.loginRequired(async (session, updatingUserId, newEmail, cb, userId) => {
 		newEmail = newEmail.toLowerCase();
-		let verificationToken = utils.generateRandomString(64);
+		let verificationToken = await utils.generateRandomString(64);
 		async.waterfall([
 			(next) => {
 				if (updatingUserId === userId) return next(null, true);
@@ -584,9 +607,9 @@ module.exports = {
 					next();
 				});
 			}
-		], (err) => {
+		], async (err) => {
 			if (err && err !== true) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("UPDATE_EMAIL", `Couldn't update email for user "${updatingUserId}" to email "${newEmail}". '${err}'`);
 				cb({status: 'failure', message: err});
 			} else {
@@ -622,9 +645,9 @@ module.exports = {
 				db.models.user.updateOne({_id: updatingUserId}, {$set: {role: newRole}}, {runValidators: true}, next);
 			}
 
-		], (err) => {
+		], async (err) => {
 			if (err && err !== true) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("UPDATE_ROLE", `User "${userId}" couldn't update role for user "${updatingUserId}" to role "${newRole}". "${err}"`);
 				cb({status: 'failure', message: err});
 			} else {
@@ -673,9 +696,9 @@ module.exports = {
 			(hashedPassword, next) => {
 				db.models.user.updateOne({_id: userId}, {$set: {"services.password.password": hashedPassword}}, next);
 			}
-		], (err) => {
+		], async (err) => {
 			if (err) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("UPDATE_PASSWORD", `Failed updating user password of user '${userId}'. '${err}'.`);
 				return cb({ status: 'failure', message: err });
 			}
@@ -696,8 +719,8 @@ module.exports = {
 	 * @param {Function} cb - gets called with the result
 	 * @param {String} userId - the userId automatically added by hooks
 	 */
-	requestPassword: hooks.loginRequired((session, cb, userId) => {
-		let code = utils.generateRandomString(8);
+	requestPassword: hooks.loginRequired(async (session, cb, userId) => {
+		let code = await utils.generateRandomString(8);
 		async.waterfall([
 			(next) => {
 				db.models.user.findOne({_id: userId}, next);
@@ -718,9 +741,9 @@ module.exports = {
 			(user, next) => {
 				mail.schemas.passwordRequest(user.email.address, user.username, code, next);
 			}
-		], (err) => {
+		], async (err) => {
 			if (err && err !== true) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("REQUEST_PASSWORD", `UserId '${userId}' failed to request password. '${err}'`);
 				cb({status: 'failure', message: err});
 			} else {
@@ -753,9 +776,9 @@ module.exports = {
 				if (user.services.password.set.expires < new Date()) return next('That code has expired.');
 				next(null);
 			}
-		], (err) => {
+		], async(err) => {
 			if (err && err !== true) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("VERIFY_PASSWORD_CODE", `Code '${code}' failed to verify. '${err}'`);
 				cb({status: 'failure', message: err});
 			} else {
@@ -807,9 +830,9 @@ module.exports = {
 			(hashedPassword, next) => {
 				db.models.user.updateOne({"services.password.set.code": code}, {$set: {"services.password.password": hashedPassword}, $unset: {"services.password.set": ''}}, {runValidators: true}, next);
 			}
-		], (err) => {
+		], async (err) => {
 			if (err && err !== true) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("ADD_PASSWORD_WITH_CODE", `Code '${code}' failed to add password. '${err}'`);
 				cb({status: 'failure', message: err});
 			} else {
@@ -841,9 +864,9 @@ module.exports = {
 				if (!user.services.github || !user.services.github.id) return next('You can\'t remove password login without having GitHub login.');
 				db.models.user.updateOne({_id: userId}, {$unset: {"services.password": ''}}, next);
 			}
-		], (err) => {
+		], async (err) => {
 			if (err && err !== true) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("UNLINK_PASSWORD", `Unlinking password failed for userId '${userId}'. '${err}'`);
 				cb({status: 'failure', message: err});
 			} else {
@@ -875,9 +898,9 @@ module.exports = {
 				if (!user.services.password || !user.services.password.password) return next('You can\'t remove GitHub login without having password login.');
 				db.models.user.updateOne({_id: userId}, {$unset: {"services.github": ''}}, next);
 			}
-		], (err) => {
+		], async (err) => {
 			if (err && err !== true) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("UNLINK_GITHUB", `Unlinking GitHub failed for userId '${userId}'. '${err}'`);
 				cb({status: 'failure', message: err});
 			} else {
@@ -898,8 +921,8 @@ module.exports = {
 	 * @param {String} email - the email of the user that requests a password reset
 	 * @param {Function} cb - gets called with the result
 	 */
-	requestPasswordReset: (session, email, cb) => {
-		let code = utils.generateRandomString(8);
+	requestPasswordReset: async (session, email, cb) => {
+		let code = await utils.generateRandomString(8);
 		async.waterfall([
 			(next) => {
 				if (!email || typeof email !== 'string') return next('Invalid email.');
@@ -922,9 +945,9 @@ module.exports = {
 			(user, next) => {
 				mail.schemas.resetPasswordRequest(user.email.address, user.username, code, next);
 			}
-		], (err) => {
+		], async (err) => {
 			if (err && err !== true) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("REQUEST_PASSWORD_RESET", `Email '${email}' failed to request password reset. '${err}'`);
 				cb({status: 'failure', message: err});
 			} else {
@@ -956,9 +979,9 @@ module.exports = {
 				if (!user.services.password.reset.expires > new Date()) return next('That code has expired.');
 				next(null);
 			}
-		], (err) => {
+		], async (err) => {
 			if (err && err !== true) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("VERIFY_PASSWORD_RESET_CODE", `Code '${code}' failed to verify. '${err}'`);
 				cb({status: 'failure', message: err});
 			} else {
@@ -1009,9 +1032,9 @@ module.exports = {
 			(hashedPassword, next) => {
 				db.models.user.updateOne({"services.password.reset.code": code}, {$set: {"services.password.password": hashedPassword}, $unset: {"services.password.reset": ''}}, {runValidators: true}, next);
 			}
-		], (err) => {
+		], async (err) => {
 			if (err && err !== true) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("CHANGE_PASSWORD_WITH_RESET_CODE", `Code '${code}' failed to change password. '${err}'`);
 				cb({status: 'failure', message: err});
 			} else {
@@ -1088,9 +1111,9 @@ module.exports = {
 				cache.pub('user.ban', {userId: value, punishment});
 				next();
 			},
-		], (err) => {
+		], async (err) => {
 			if (err && err !== true) {
-				err = utils.getError(err);
+				err = await utils.getError(err);
 				logger.error("BAN_USER_BY_ID", `User ${userId} failed to ban user ${value} with the reason ${reason}. '${err}'`);
 				cb({status: 'failure', message: err});
 			} else {

+ 32 - 26
backend/logic/api.js

@@ -1,34 +1,40 @@
-let lockdown = false;
+const coreClass = require("../core");
 
-module.exports = {
-	init: (cb) => {
-		const { app } = require('./app.js');
-		const actions = require('./actions');
+module.exports = class extends coreClass {
+	constructor(name, moduleManager) {
+		super(name, moduleManager);
 
-		app.get('/', (req, res) => {
-			res.json({
-				status: 'success',
-				message: 'Coming Soon'
-			});
-		});
+		this.dependsOn = ["app", "db", "cache"];
+	}
 
-		Object.keys(actions).forEach((namespace) => {
-			Object.keys(actions[namespace]).forEach((action) => {
-				let name = `/${namespace}/${action}`;
+	initialize() {
+		return new Promise((resolve, reject) => {
+			this.setStage(1);
 
-				app.get(name, (req, res) => {
-					actions[namespace][action](null, (result) => {
-						if (typeof cb === 'function') return res.json(result);
-					});
+			this.app = this.moduleManager.modules["app"];
+
+			this.app.app.get('/', (req, res) => {
+				res.json({
+					status: 'success',
+					message: 'Coming Soon'
 				});
-			})
-		});
+			});
 
-		if (lockdown) return this._lockdown();
-		cb();
-	},
+			const actions = require("../logic/actions");
+	
+			Object.keys(actions).forEach((namespace) => {
+				Object.keys(actions[namespace]).forEach((action) => {
+					let name = `/${namespace}/${action}`;
+	
+					this.app.app.get(name, (req, res) => {
+						actions[namespace][action](null, (result) => {
+							if (typeof cb === 'function') return res.json(result);
+						});
+					});
+				})
+			});
 
-	_lockdown: () => {
-		lockdown = true;
+			resolve();
+		});
 	}
-}
+}

+ 202 - 214
backend/logic/app.js

@@ -1,251 +1,239 @@
 'use strict';
 
+const coreClass = require("../core");
+
 const express = require('express');
 const bodyParser = require('body-parser');
 const cookieParser = require('cookie-parser');
 const cors = require('cors');
 const config = require('config');
 const async = require('async');
-const logger = require('./logger');
-const mail = require('./mail');
 const request = require('request');
 const OAuth2 = require('oauth').OAuth2;
 
-const api = require('./api');
-const cache = require('./cache');
-const db = require('./db');
+module.exports = class extends coreClass {
+	initialize() {
+		return new Promise((resolve, reject) => {
+			this.setStage(1);
 
-let utils;
-let initialized = false;
-let lockdown = false;
+			const 	logger 	= this.logger,
+					mail	= this.moduleManager.modules["mail"],
+					cache	= this.moduleManager.modules["cache"],
+					db		= this.moduleManager.modules["db"];
+			
+			this.utils = this.moduleManager.modules["utils"];
 
-const lib = {
+			let app = this.app = express();
 
-	app: null,
-	server: null,
+			this.server = app.listen(config.get('serverPort'));
 
-	init: (cb) => {
+			app.use(cookieParser());
 
-		utils = require('./utils');
+			app.use(bodyParser.json());
+			app.use(bodyParser.urlencoded({ extended: true }));
 
-		let app = lib.app = express();
+			let corsOptions = Object.assign({}, config.get('cors'));
 
-		lib.server = app.listen(config.get('serverPort'));
+			app.use(cors(corsOptions));
+			app.options('*', cors(corsOptions));
 
-		app.use(cookieParser());
+			let oauth2 = new OAuth2(
+				config.get('apis.github.client'),
+				config.get('apis.github.secret'),
+				'https://github.com/',
+				'login/oauth/authorize',
+				'login/oauth/access_token',
+				null
+			);
 
-		app.use(bodyParser.json());
-		app.use(bodyParser.urlencoded({ extended: true }));
+			let redirect_uri = config.get('serverDomain') + '/auth/github/authorize/callback';
 
-		let corsOptions = Object.assign({}, config.get('cors'));
+			app.get('/auth/github/authorize', async (req, res) => {
+				try { await this._validateHook(); } catch { return; }
+				let params = [
+					`client_id=${config.get('apis.github.client')}`,
+					`redirect_uri=${config.get('serverDomain')}/auth/github/authorize/callback`,
+					`scope=user:email`
+				].join('&');
+				res.redirect(`https://github.com/login/oauth/authorize?${params}`);
+			});
 
-		app.use(cors(corsOptions));
-		app.options('*', cors(corsOptions));
+			app.get('/auth/github/link', async (req, res) => {
+				try { await this._validateHook(); } catch { return; }
+				let params = [
+					`client_id=${config.get('apis.github.client')}`,
+					`redirect_uri=${config.get('serverDomain')}/auth/github/authorize/callback`,
+					`scope=user:email`,
+					`state=${req.cookies.SID}`
+				].join('&');
+				res.redirect(`https://github.com/login/oauth/authorize?${params}`);
+			});
 
-		let oauth2 = new OAuth2(
-			config.get('apis.github.client'),
-			config.get('apis.github.secret'),
-			'https://github.com/',
-			'login/oauth/authorize',
-			'login/oauth/access_token',
-			null
-		);
+			function redirectOnErr (res, err){
+				return res.redirect(`${config.get('domain')}/?err=${encodeURIComponent(err)}`);
+			}
+
+			app.get('/auth/github/authorize/callback', async (req, res) => {
+				try { await this._validateHook(); } catch { return; }
+				let code = req.query.code;
+				let access_token;
+				let body;
+				let address;
+				const state = req.query.state;
+
+				async.waterfall([
+					(next) => {
+						oauth2.getOAuthAccessToken(code, {redirect_uri}, next);
+					},
+
+					(_access_token, refresh_token, results, next) => {
+						access_token = _access_token;
+						request.get({
+							url: `https://api.github.com/user?access_token=${access_token}`,
+							headers: {'User-Agent': 'request'}
+						}, next);
+					},
+
+					(httpResponse, _body, next) => {
+						body = _body = JSON.parse(_body);
+						if (state) {
+							return async.waterfall([
+								(next) => {
+									cache.hget('sessions', state, next);
+								},
+
+								(session, next) => {
+									if (!session) return next('Invalid session.');
+									db.models.user.findOne({_id: session.userId}, next);
+								},
+
+								(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.updateOne({_id: user._id}, {$set: {"services.github": {id: body.id, access_token}}}, {runValidators: true}, (err) => {
+										if (err) return next(err);
+										next(null, user, body);
+									});
+								},
+
+								(user) => {
+									cache.pub('user.linkGitHub', user._id);
+									res.redirect(`${config.get('domain')}/settings`);
+								}
+							], next);
+						}
+						db.models.user.findOne({'services.github.id': body.id}, (err, user) => {
+							next(err, user, body);
+						});
+					},
+
+					(user, body, next) => {
+						if (user) {
+							user.services.github.access_token = access_token;
+							return user.save(() => {
+								next(true, user._id);
+							});
+						}
+						db.models.user.findOne({ username: new RegExp(`^${body.login}$`, 'i' )}, (err, user) => {
+							next(err, user);
+						});
+					},
+
+					(user, next) => {
+						if (user) return next('An account with that username already exists.');
+						request.get({
+							url: `https://api.github.com/user/emails?access_token=${access_token}`,
+							headers: {'User-Agent': 'request'}
+						}, next);
+					},
+
+					(httpResponse, body2, next) => {
+						body2 = JSON.parse(body2);
+						if (!Array.isArray(body2)) return next(body2.message);
+						body2.forEach(email => {
+							if (email.primary) address = email.email.toLowerCase();
+						});
+						db.models.user.findOne({'email.address': address}, next);
+					},
+
+					async (user, next) => {
+						const verificationToken = await this.utils.generateRandomString(64);
+						if (user) return next('An account with that email address already exists.');
+						db.models.user.create({
+							_id: await this.utils.generateRandomString(12),//TODO Check if exists
+							username: body.login,
+							email: {
+								address,
+								verificationToken: verificationToken
+							},
+							services: {
+								github: {id: body.id, access_token}
+							}
+						}, next);
+					},
 
-		let redirect_uri = config.get('serverDomain') + '/auth/github/authorize/callback';
+					(user, next) => {
+						mail.schemas.verifyEmail(address, body.login, user.email.verificationToken);
+						next(null, user._id);
+					}
+				], async (err, userId) => {
+					if (err && err !== true) {
+						err = await this.utils.getError(err);
+						logger.error('AUTH_GITHUB_AUTHORIZE_CALLBACK', `Failed to authorize with GitHub. "${err}"`);
+						return redirectOnErr(res, err);
+					}
 
-		app.get('/auth/github/authorize', (req, res) => {
-			if (lockdown) return res.json({status: 'failure', message: 'Lockdown'});
-			let params = [
-				`client_id=${config.get('apis.github.client')}`,
-				`redirect_uri=${config.get('serverDomain')}/auth/github/authorize/callback`,
-				`scope=user:email`
-			].join('&');
-			res.redirect(`https://github.com/login/oauth/authorize?${params}`);
-		});
+					const sessionId = await this.utils.guid();
+					cache.hset('sessions', sessionId, cache.schemas.session(sessionId, userId), err => {
+						if (err) return redirectOnErr(res, err.message);
+						let date = new Date();
+						date.setTime(new Date().getTime() + (2 * 365 * 24 * 60 * 60 * 1000));
+						res.cookie('SID', sessionId, {
+							expires: date,
+							secure: config.get("cookie.secure"),
+							path: "/",
+							domain: config.get("cookie.domain")
+						});
+						logger.success('AUTH_GITHUB_AUTHORIZE_CALLBACK', `User "${userId}" successfully authorized with GitHub.`);
+						res.redirect(`${config.get('domain')}/`);
+					});
+				});
+			});
 
-		app.get('/auth/github/link', (req, res) => {
-			if (lockdown) return res.json({status: 'failure', message: 'Lockdown'});
-			let params = [
-				`client_id=${config.get('apis.github.client')}`,
-				`redirect_uri=${config.get('serverDomain')}/auth/github/authorize/callback`,
-				`scope=user:email`,
-				`state=${req.cookies.SID}`
-			].join('&');
-			res.redirect(`https://github.com/login/oauth/authorize?${params}`);
-		});
+			app.get('/auth/verify_email', async (req, res) => {
+				try { await this._validateHook(); } catch { return; }
 
-		function redirectOnErr (res, err){
-			return res.redirect(`${config.get('domain')}/?err=${encodeURIComponent(err)}`);
-		}
-
-		app.get('/auth/github/authorize/callback', (req, res) => {
-			if (lockdown) return res.json({status: 'failure', message: 'Lockdown'});
-			let code = req.query.code;
-			let access_token;
-			let body;
-			let address;
-			const state = req.query.state;
-
-			async.waterfall([
-				(next) => {
-					oauth2.getOAuthAccessToken(code, {redirect_uri}, next);
-				},
-
-				(_access_token, refresh_token, results, next) => {
-					access_token = _access_token;
-					request.get({
-						url: `https://api.github.com/user?access_token=${access_token}`,
-						headers: {'User-Agent': 'request'}
-					}, next);
-				},
-
-				(httpResponse, _body, next) => {
-					body = _body = JSON.parse(_body);
-					if (state) {
-						return async.waterfall([
-							(next) => {
-								cache.hget('sessions', state, next);
-							},
+				let code = req.query.code;
 
-							(session, next) => {
-								if (!session) return next('Invalid session.');
-								db.models.user.findOne({_id: session.userId}, next);
-							},
+				async.waterfall([
+					(next) => {
+						if (!code) return next('Invalid code.');
+						next();
+					},
 
-							(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.updateOne({_id: user._id}, {$set: {"services.github": {id: body.id, access_token}}}, {runValidators: true}, (err) => {
-									if (err) return next(err);
-									next(null, user, body);
-								});
-							},
+					(next) => {
+						db.models.user.findOne({"email.verificationToken": code}, next);
+					},
 
-							(user) => {
-								cache.pub('user.linkGitHub', user._id);
-								res.redirect(`${config.get('domain')}/settings`);
-							}
-						], next);
+					(user, next) => {
+						if (!user) return next('User not found.');
+						if (user.email.verified) return next('This email is already verified.');
+						db.models.user.updateOne({"email.verificationToken": code}, {$set: {"email.verified": true}, $unset: {"email.verificationToken": ''}}, {runValidators: true}, next);
 					}
-					db.models.user.findOne({'services.github.id': body.id}, (err, user) => {
-						next(err, user, body);
-					});
-				},
-
-				(user, body, next) => {
-					if (user) {
-						user.services.github.access_token = access_token;
-						return user.save(() => {
-							next(true, user._id);
-						});
+				], (err) => {
+					if (err) {
+						let error = 'An error occurred.';
+						if (typeof err === "string") error = err;
+						else if (err.message) error = err.message;
+						logger.error("VERIFY_EMAIL", `Verifying email failed. "${error}"`);
+						return res.json({ status: 'failure', message: error});
 					}
-					db.models.user.findOne({ username: new RegExp(`^${body.login}$`, 'i' )}, (err, user) => {
-						next(err, user);
-					});
-				},
-
-				(user, next) => {
-					if (user) return next('An account with that username already exists.');
-					request.get({
-						url: `https://api.github.com/user/emails?access_token=${access_token}`,
-						headers: {'User-Agent': 'request'}
-					}, next);
-				},
-
-				(httpResponse, body2, next) => {
-					body2 = JSON.parse(body2);
-					if (!Array.isArray(body2)) return next(body2.message);
-					body2.forEach(email => {
-						if (email.primary) address = email.email.toLowerCase();
-					});
-					db.models.user.findOne({'email.address': address}, next);
-				},
-
-				(user, next) => {
-					const verificationToken = utils.generateRandomString(64);
-					if (user) return next('An account with that email address already exists.');
-					db.models.user.create({
-						_id: utils.generateRandomString(12),//TODO Check if exists
-						username: body.login,
-						email: {
-							address,
-							verificationToken: verificationToken
-						},
-						services: {
-							github: {id: body.id, access_token}
-						}
-					}, next);
-				},
-
-				(user, next) => {
-					mail.schemas.verifyEmail(address, body.login, user.email.verificationToken);
-					next(null, user._id);
-				}
-			], (err, userId) => {
-				if (err && err !== true) {
-					err = utils.getError(err);
-					logger.error('AUTH_GITHUB_AUTHORIZE_CALLBACK', `Failed to authorize with GitHub. "${err}"`);
-					return redirectOnErr(res, err);
-				}
-
-				const sessionId = utils.guid();
-				cache.hset('sessions', sessionId, cache.schemas.session(sessionId, userId), err => {
-					if (err) return redirectOnErr(res, err.message);
-					let date = new Date();
-					date.setTime(new Date().getTime() + (2 * 365 * 24 * 60 * 60 * 1000));
-					res.cookie('SID', sessionId, {
-						expires: date,
-						secure: config.get("cookie.secure"),
-						path: "/",
-						domain: config.get("cookie.domain")
-					});
-					logger.success('AUTH_GITHUB_AUTHORIZE_CALLBACK', `User "${userId}" successfully authorized with GitHub.`);
-					res.redirect(`${config.get('domain')}/`);
+					logger.success("VERIFY_EMAIL", `Successfully verified email.`);
+					res.redirect(`${config.get("domain")}?msg=Thank you for verifying your email`);
 				});
 			});
-		});
 
-		app.get('/auth/verify_email', (req, res) => {
-			let code = req.query.code;
-
-			async.waterfall([
-				(next) => {
-					if (!code) return next('Invalid code.');
-					next();
-				},
-
-				(next) => {
-					db.models.user.findOne({"email.verificationToken": code}, next);
-				},
-
-				(user, next) => {
-					if (!user) return next('User not found.');
-					if (user.email.verified) return next('This email is already verified.');
-					db.models.user.updateOne({"email.verificationToken": code}, {$set: {"email.verified": true}, $unset: {"email.verificationToken": ''}}, {runValidators: true}, next);
-				}
-			], (err) => {
-				if (err) {
-					let error = 'An error occurred.';
-					if (typeof err === "string") error = err;
-					else if (err.message) error = err.message;
-					logger.error("VERIFY_EMAIL", `Verifying email failed. "${error}"`);
-					return res.json({ status: 'failure', message: error});
-				}
-				logger.success("VERIFY_EMAIL", `Successfully verified email.`);
-				res.redirect(`${config.get("domain")}?msg=Thank you for verifying your email`);
-			});
+			resolve();
 		});
-
-		initialized = true;
-
-		if (lockdown) return this._lockdown();
-		cb();
-	},
-
-	_lockdown: () => {
-		lib.server.close();
-		lockdown = true;
 	}
-};
-
-module.exports = lib;
+}

+ 81 - 91
backend/logic/cache/index.js

@@ -1,67 +1,64 @@
 'use strict';
 
+const coreClass = require("../../core");
+
 const redis = require('redis');
+const config = require('config');
 const mongoose = require('mongoose');
 
 // Lightweight / convenience wrapper around redis module for our needs
 
 const pubs = {}, subs = {};
-let callbacks = [];
-let initialized = false;
-let lockdown = false;
-
-const lib = {
-
-	client: null,
-	errorCb: null,
-	url: '',
-	schemas: {
-		session: require('./schemas/session'),
-		station: require('./schemas/station'),
-		playlist: require('./schemas/playlist'),
-		officialPlaylist: require('./schemas/officialPlaylist'),
-		song: require('./schemas/song'),
-		punishment: require('./schemas/punishment')
-	},
 
-	/**
-	 * Initializes the cache module
-	 *
-	 * @param {String} url - the url of the redis server
-	 * @param {String} password - the password of the redis server
-	 * @param {Function} cb - gets called once we're done initializing
-	 */
-	init: (url, password, errorCb, cb) => {
-		lib.errorCb = errorCb;
-		lib.url = url;
-		lib.password = password;
-
-		lib.client = redis.createClient({ url: lib.url, password: lib.password });
-		lib.client.on('error', (err) => {
-			if (lockdown) return;
-			errorCb('Cache connection error.', err, 'Cache');
-		});
+module.exports = class extends coreClass {
+	initialize() {
+		return new Promise((resolve, reject) => {
+			this.setStage(1);
+
+			this.schemas = {
+				session: require('./schemas/session'),
+				station: require('./schemas/station'),
+				playlist: require('./schemas/playlist'),
+				officialPlaylist: require('./schemas/officialPlaylist'),
+				song: require('./schemas/song'),
+				punishment: require('./schemas/punishment')
+			}
 
-		callbacks.forEach((callback) => {
-			callback();
-		});
+			this.url = config.get("redis").url;
+			this.password = config.get("redis").password;
 
-		initialized = true;
+			this.logger.info("REDIS", "Connecting...");
 
-		if (lockdown) return this._lockdown();
-		cb();
-	},
+			this.client = redis.createClient({
+				url: this.url,
+				password: this.password
+			});
+
+			this.client.on('error', err => {
+				if (this.lockdown) return;
+				//errorCb('Cache connection error.', err, 'Cache');
+				console.log("REDIS ERROR " + err);
+				reject(err);
+			});
+
+			this.client.on("connect", () => {
+				resolve();
+			});
+		});
+	}
 
 	/**
 	 * Gracefully closes all the Redis client connections
 	 */
-	quit: () => {
-		if (lib.client.connected) {
-			lib.client.quit();
+	async quit() {
+		try { await this._validateHook(); } catch { return; }
+
+		if (this.client.connected) {
+			this.client.quit();
 			Object.keys(pubs).forEach((channel) => pubs[channel].quit());
 			Object.keys(subs).forEach((channel) => subs[channel].client.quit());
 		}
-	},
+	}
 
 	/**
 	 * Sets a single value in a table
@@ -72,19 +69,20 @@ const lib = {
 	 * @param {Function} cb - gets called when the value has been set in Redis
 	 * @param {Boolean} [stringifyJson=true] - stringify 'value' if it's an Object or Array
 	 */
-	hset: (table, key, value, cb, stringifyJson = true) => {
-		if (lockdown) return cb('Lockdown');
+	async hset(table, key, value, cb, stringifyJson = true) {
+		try { await this._validateHook(); } catch { return; }
+
 		if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();
 		// automatically stringify objects and arrays into JSON
 		if (stringifyJson && ['object', 'array'].includes(typeof value)) value = JSON.stringify(value);
 
-		lib.client.hset(table, key, value, err => {
+		this.client.hset(table, key, value, err => {
 			if (cb !== undefined) {
 				if (err) return cb(err);
 				cb(null, JSON.parse(value));
 			}
 		});
-	},
+	}
 
 	/**
 	 * Gets a single value from a table
@@ -94,11 +92,13 @@ const lib = {
 	 * @param {Function} cb - gets called when the value is returned from Redis
 	 * @param {Boolean} [parseJson=true] - attempt to parse returned data as JSON
 	 */
-	hget: (table, key, cb, parseJson = true) => {
-		if (lockdown) return cb('Lockdown');
+	async hget(table, key, cb, parseJson = true) {
+		try { await this._validateHook(); } catch { return; }
+
 		if (!key || !table) return typeof cb === 'function' ? cb(null, null) : null;
 		if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();
-		lib.client.hget(table, key, (err, value) => {
+
+		this.client.hget(table, key, (err, value) => {
 			if (err) return typeof cb === 'function' ? cb(err) : null;
 			if (parseJson) try {
 				value = JSON.parse(value);
@@ -106,7 +106,7 @@ const lib = {
 			}
 			if (typeof cb === 'function') cb(null, value);
 		});
-	},
+	}
 
 	/**
 	 * Deletes a single value from a table
@@ -115,15 +115,17 @@ const lib = {
 	 * @param {String} key - name of the key to delete
 	 * @param {Function} cb - gets called when the value has been deleted from Redis or when it returned an error
 	 */
-	hdel: (table, key, cb) => {
-		if (lockdown) return cb('Lockdown');
+	async hdel(table, key, cb) {
+		try { await this._validateHook(); } catch { return; }
+
 		if (!key || !table) return cb(null, null);
 		if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();
-		lib.client.hdel(table, key, (err) => {
+
+		this.client.hdel(table, key, (err) => {
 			if (err) return cb(err);
 			else return cb(null);
 		});
-	},
+	}
 
 	/**
 	 * Returns all the keys for a table
@@ -132,16 +134,18 @@ const lib = {
 	 * @param {Function} cb - gets called when the values are returned from Redis
 	 * @param {Boolean} [parseJson=true] - attempts to parse all values as JSON by default
 	 */
-	hgetall: (table, cb, parseJson = true) => {
-		if (lockdown) return cb('Lockdown');
+	async hgetall(table, cb, parseJson = true) {
+		try { await this._validateHook(); } catch { return; }
+
 		if (!table) return cb(null, null);
-		lib.client.hgetall(table, (err, obj) => {
+
+		this.client.hgetall(table, (err, obj) => {
 			if (err) return typeof cb === 'function' ? cb(err) : null;
 			if (parseJson && obj) Object.keys(obj).forEach((key) => { try { obj[key] = JSON.parse(obj[key]); } catch (e) {} });
 			if (parseJson && !obj) obj = [];
 			cb(null, obj);
 		});
-	},
+	}
 
 	/**
 	 * Publish a message to a channel, caches the redis client connection
@@ -150,18 +154,18 @@ const lib = {
 	 * @param {*} value - the value we want to send
 	 * @param {Boolean} [stringifyJson=true] - stringify 'value' if it's an Object or Array
 	 */
-	pub: (channel, value, stringifyJson = true) => {
-
+	async pub(channel, value, stringifyJson = true) {
+		try { await this._validateHook(); } catch { return; }
 		/*if (pubs[channel] === undefined) {
-		 pubs[channel] = redis.createClient({ url: lib.url });
+		 pubs[channel] = redis.createClient({ url: this.url });
 		 pubs[channel].on('error', (err) => console.error);
 		 }*/
 
 		if (stringifyJson && ['object', 'array'].includes(typeof value)) value = JSON.stringify(value);
 
 		//pubs[channel].publish(channel, value);
-		lib.client.publish(channel, value);
-	},
+		this.client.publish(channel, value);
+	}
 
 	/**
 	 * Subscribe to a channel, caches the redis client connection
@@ -170,32 +174,18 @@ const lib = {
 	 * @param {Function} cb - gets called when a message is received
 	 * @param {Boolean} [parseJson=true] - parse the message as JSON
 	 */
-	sub: (channel, cb, parseJson = true) => {
-		if (lockdown) return;
-		if (initialized) subToChannel();
-		else {
-			callbacks.push(() => {
-				subToChannel();
+	async sub(channel, cb, parseJson = true) {
+		try { await this._validateHook(); } catch { return; }
+
+		if (subs[channel] === undefined) {
+			subs[channel] = { client: redis.createClient({ url: this.url, password: this.password }), cbs: [] };
+			subs[channel].client.on('message', (channel, message) => {
+				if (parseJson) try { message = JSON.parse(message); } catch (e) {}
+				subs[channel].cbs.forEach((cb) => cb(message));
 			});
+			subs[channel].client.subscribe(channel);
 		}
-		function subToChannel() {
-			if (subs[channel] === undefined) {
-				subs[channel] = { client: redis.createClient({ url: lib.url, password: lib.password }), cbs: [] };
-				subs[channel].client.on('message', (channel, message) => {
-					if (parseJson) try { message = JSON.parse(message); } catch (e) {}
-					subs[channel].cbs.forEach((cb) => cb(message));
-				});
-				subs[channel].client.subscribe(channel);
-			}
-
-			subs[channel].cbs.push(cb);
-		}
-	},
 
-	_lockdown: () => {
-		lib.quit();
-		lockdown = true;
+		subs[channel].cbs.push(cb);
 	}
-};
-
-module.exports = lib;
+}

+ 179 - 182
backend/logic/db/index.js

@@ -1,10 +1,10 @@
 'use strict';
 
+const coreClass = require("../../core");
+
 const mongoose = require('mongoose');
 const config = require('config');
 
-const bluebird = require('bluebird');
-
 const regex = {
 	azAZ09_: /^[A-Za-z0-9_]+$/,
 	az09_: /^[a-z0-9_]+$/,
@@ -17,194 +17,191 @@ const isLength = (string, min, max) => {
 	return !(typeof string !== 'string' || string.length < min || string.length > max);
 }
 
+const bluebird = require('bluebird');
+
 mongoose.Promise = bluebird;
 
-let initialized = false;
-let lockdown = false;
+module.exports = class extends coreClass {
+	initialize() {
+		return new Promise((resolve, reject) => {
+			this.setStage(1);
 
-let lib = {
+			this.schemas = {};
+			this.models = {};
 
-	connection: null,
-	schemas: {},
-	models: {},
+			const mongoUrl = config.get("mongo").url;
 
-	init: (url, errorCb,  cb) => {
-		mongoose.connect(url, {
-			useNewUrlParser: true,
-			useCreateIndex: true
-		})
-			.then(() => {
-				lib.schemas = {
-					song: new mongoose.Schema(require(`./schemas/song`)),
-					queueSong: new mongoose.Schema(require(`./schemas/queueSong`)),
-					station: new mongoose.Schema(require(`./schemas/station`)),
-					user: new mongoose.Schema(require(`./schemas/user`)),
-					playlist: new mongoose.Schema(require(`./schemas/playlist`)),
-					news: new mongoose.Schema(require(`./schemas/news`)),
-					report: new mongoose.Schema(require(`./schemas/report`)),
-					punishment: new mongoose.Schema(require(`./schemas/punishment`))
-				};
-	
-				lib.models = {
-					song: mongoose.model('song', lib.schemas.song),
-					queueSong: mongoose.model('queueSong', lib.schemas.queueSong),
-					station: mongoose.model('station', lib.schemas.station),
-					user: mongoose.model('user', lib.schemas.user),
-					playlist: mongoose.model('playlist', lib.schemas.playlist),
-					news: mongoose.model('news', lib.schemas.news),
-					report: mongoose.model('report', lib.schemas.report),
-					punishment: mongoose.model('punishment', lib.schemas.punishment)
-				};
-	
-				// 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 (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) => {
-						return character.charCodeAt(0) === 21328;
-					}).length === 0;
-				}, 'Invalid display name.');
-	
-	
-				lib.schemas.station.path('owner').validate((owner, callback) => {
-					lib.models.station.countDocuments({ owner: owner }, (err, c) => {
-						callback(!(err || c >= 3));
-					});
-				}, 'User already has 3 stations.');
-	
-				/*
-				lib.schemas.station.path('queue').validate((queue, callback) => {
-					let totalDuration = 0;
-					queue.forEach((song) => {
-						totalDuration += song.duration;
+			mongoose.connect(mongoUrl, {
+				useNewUrlParser: true,
+				useCreateIndex: true
+			})
+				.then(() => {
+					this.schemas = {
+						song: new mongoose.Schema(require(`./schemas/song`)),
+						queueSong: new mongoose.Schema(require(`./schemas/queueSong`)),
+						station: new mongoose.Schema(require(`./schemas/station`)),
+						user: new mongoose.Schema(require(`./schemas/user`)),
+						playlist: new mongoose.Schema(require(`./schemas/playlist`)),
+						news: new mongoose.Schema(require(`./schemas/news`)),
+						report: new mongoose.Schema(require(`./schemas/report`)),
+						punishment: new mongoose.Schema(require(`./schemas/punishment`))
+					};
+		
+					this.models = {
+						song: mongoose.model('song', this.schemas.song),
+						queueSong: mongoose.model('queueSong', this.schemas.queueSong),
+						station: mongoose.model('station', this.schemas.station),
+						user: mongoose.model('user', this.schemas.user),
+						playlist: mongoose.model('playlist', this.schemas.playlist),
+						news: mongoose.model('news', this.schemas.news),
+						report: mongoose.model('report', this.schemas.report),
+						punishment: mongoose.model('punishment', this.schemas.punishment)
+					};
+		
+					// this.schemas.user.path('username').validate((username) => {
+					// 	return (isLength(username, 2, 32) && regex.azAZ09_.test(username));
+					// }, 'Invalid username.');
+		
+					this.schemas.user.path('email.address').validate((email) => {
+						if (!isLength(email, 3, 254)) return false;
+						if (email.indexOf('@') !== email.lastIndexOf('@')) return false;
+						return regex.emailSimple.test(email);
+					}, 'Invalid email.');
+		
+					this.schemas.station.path('name').validate((id) => {
+						return (isLength(id, 2, 16) && regex.az09_.test(id));
+					}, 'Invalid station name.');
+		
+					this.schemas.station.path('displayName').validate((displayName) => {
+						return (isLength(displayName, 2, 32) && regex.azAZ09_.test(displayName));
+					}, 'Invalid display name.');
+		
+					this.schemas.station.path('description').validate((description) => {
+						if (!isLength(description, 2, 200)) return false;
+						let characters = description.split("");
+						return characters.filter((character) => {
+							return character.charCodeAt(0) === 21328;
+						}).length === 0;
+					}, 'Invalid display name.');
+		
+		
+					this.schemas.station.path('owner').validate({
+						isAsync: true,
+						validator: (owner, callback) => {
+							this.models.station.countDocuments({ owner: owner }, (err, c) => {
+								callback(!(err || c >= 3))
+							});
+						},
+						message: 'User already has 3 stations.'
 					});
-					return callback(totalDuration <= 3600 * 3);
-				}, 'The max length of the queue is 3 hours.');
-	
-				lib.schemas.station.path('queue').validate((queue, callback) => {
-					if (queue.length === 0) return callback(true);
-					let totalDuration = 0;
-					const userId = queue[queue.length - 1].requestedBy;
-					queue.forEach((song) => {
-						if (userId === song.requestedBy) {
+		
+					/*
+					this.schemas.station.path('queue').validate((queue, callback) => { //Callback no longer works, see station max count
+						let totalDuration = 0;
+						queue.forEach((song) => {
 							totalDuration += song.duration;
-						}
-					});
-					return callback(totalDuration <= 900);
-				}, 'The max length of songs per user is 15 minutes.');
-	
-				lib.schemas.station.path('queue').validate((queue, callback) => {
-					if (queue.length === 0) return callback(true);
-					let totalSongs = 0;
-					const userId = queue[queue.length - 1].requestedBy;
-					queue.forEach((song) => {
-						if (userId === song.requestedBy) {
-							totalSongs++;
-						}
-					});
-					if (totalSongs <= 2) return callback(true);
-					if (totalSongs > 3) return callback(false);
-					if (queue[queue.length - 2].requestedBy !== userId || queue[queue.length - 3] !== userId) return callback(true);
-					return callback(false);
-				}, 'The max amount of songs per user is 3, and only 2 in a row is allowed.');
-				*/
-	
-				let songTitle = (title) => {
-					return isLength(title, 1, 100);
-				};
-				lib.schemas.song.path('title').validate(songTitle, 'Invalid title.');
-				lib.schemas.queueSong.path('title').validate(songTitle, 'Invalid title.');
-	
-				lib.schemas.song.path('artists').validate((artists) => {
-					return !(artists.length < 1 || artists.length > 10);
-				}, 'Invalid artists.');
-				lib.schemas.queueSong.path('artists').validate((artists) => {
-					return !(artists.length < 0 || artists.length > 10);
-				}, 'Invalid artists.');
-	
-				let songArtists = (artists) => {
-					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.');
-	
-				lib.schemas.song.path('thumbnail').validate((thumbnail) => {
-					return isLength(thumbnail, 8, 256);
-				}, 'Invalid thumbnail.');
-				lib.schemas.queueSong.path('thumbnail').validate((thumbnail) => {
-					return isLength(thumbnail, 0, 256);
-				}, 'Invalid thumbnail.');
-	
-				lib.schemas.playlist.path('displayName').validate((displayName) => {
-					return (isLength(displayName, 1, 16) && regex.ascii.test(displayName));
-				}, 'Invalid display name.');
-	
-				lib.schemas.playlist.path('createdBy').validate((createdBy) => {
-					lib.models.playlist.countDocuments({ createdBy: createdBy }, (err, c) => {
-						return !(err || c >= 10);
-					});
-				}, 'Max 10 playlists per user.');
-	
-				lib.schemas.playlist.path('songs').validate((songs) => {
-					return songs.length <= 2000;
-				}, 'Max 2000 songs per playlist.');
-	
-				lib.schemas.playlist.path('songs').validate((songs) => {
-					if (songs.length === 0) return true;
-					return songs[0].duration <= 10800;
-				}, 'Max 3 hours per song.');
-	
-				lib.schemas.report.path('description').validate((description) => {
-					return (!description || (isLength(description, 0, 400) && regex.ascii.test(description)));
-				}, 'Invalid description.');
-	
-				initialized = true;
-	
-				if (lockdown) return this._lockdown();
-				cb();
-			})
-			.catch(err => {
-				console.error(err);
-				errorCb('Database connection error.', err, 'DB');
-			});
-	},
+						});
+						return callback(totalDuration <= 3600 * 3);
+					}, 'The max length of the queue is 3 hours.');
+		
+					this.schemas.station.path('queue').validate((queue, callback) => { //Callback no longer works, see station max count
+						if (queue.length === 0) return callback(true);
+						let totalDuration = 0;
+						const userId = queue[queue.length - 1].requestedBy;
+						queue.forEach((song) => {
+							if (userId === song.requestedBy) {
+								totalDuration += song.duration;
+							}
+						});
+						return callback(totalDuration <= 900);
+					}, 'The max length of songs per user is 15 minutes.');
+		
+					this.schemas.station.path('queue').validate((queue, callback) => { //Callback no longer works, see station max count
+						if (queue.length === 0) return callback(true);
+						let totalSongs = 0;
+						const userId = queue[queue.length - 1].requestedBy;
+						queue.forEach((song) => {
+							if (userId === song.requestedBy) {
+								totalSongs++;
+							}
+						});
+						if (totalSongs <= 2) return callback(true);
+						if (totalSongs > 3) return callback(false);
+						if (queue[queue.length - 2].requestedBy !== userId || queue[queue.length - 3] !== userId) return callback(true);
+						return callback(false);
+					}, 'The max amount of songs per user is 3, and only 2 in a row is allowed.');
+					*/
+		
+					let songTitle = (title) => {
+						return isLength(title, 1, 100);
+					};
+					this.schemas.song.path('title').validate(songTitle, 'Invalid title.');
+					this.schemas.queueSong.path('title').validate(songTitle, 'Invalid title.');
+		
+					this.schemas.song.path('artists').validate((artists) => {
+						return !(artists.length < 1 || artists.length > 10);
+					}, 'Invalid artists.');
+					this.schemas.queueSong.path('artists').validate((artists) => {
+						return !(artists.length < 0 || artists.length > 10);
+					}, 'Invalid artists.');
+		
+					let songArtists = (artists) => {
+						return artists.filter((artist) => {
+								return (isLength(artist, 1, 32) && regex.ascii.test(artist) && artist !== "NONE");
+							}).length === artists.length;
+					};
+					this.schemas.song.path('artists').validate(songArtists, 'Invalid artists.');
+					this.schemas.queueSong.path('artists').validate(songArtists, 'Invalid artists.');
+		
+					let songGenres = (genres) => {
+						return genres.filter((genre) => {
+								return (isLength(genre, 1, 16) && regex.az09_.test(genre));
+							}).length === genres.length;
+					};
+					this.schemas.song.path('genres').validate(songGenres, 'Invalid genres.');
+					this.schemas.queueSong.path('genres').validate(songGenres, 'Invalid genres.');
+		
+					this.schemas.song.path('thumbnail').validate((thumbnail) => {
+						return isLength(thumbnail, 8, 256);
+					}, 'Invalid thumbnail.');
+					this.schemas.queueSong.path('thumbnail').validate((thumbnail) => {
+						return isLength(thumbnail, 0, 256);
+					}, 'Invalid thumbnail.');
+		
+					this.schemas.playlist.path('displayName').validate((displayName) => {
+						return (isLength(displayName, 1, 16) && regex.ascii.test(displayName));
+					}, 'Invalid display name.');
+		
+					this.schemas.playlist.path('createdBy').validate((createdBy) => {
+						this.models.playlist.countDocuments({ createdBy: createdBy }, (err, c) => {
+							return !(err || c >= 10);
+						});
+					}, 'Max 10 playlists per user.');
+		
+					this.schemas.playlist.path('songs').validate((songs) => {
+						return songs.length <= 2000;
+					}, 'Max 2000 songs per playlist.');
+		
+					this.schemas.playlist.path('songs').validate((songs) => {
+						if (songs.length === 0) return true;
+						return songs[0].duration <= 10800;
+					}, 'Max 3 hours per song.');
+		
+					this.schemas.report.path('description').validate((description) => {
+						return (!description || (isLength(description, 0, 400) && regex.ascii.test(description)));
+					}, 'Invalid description.');
+
+					resolve();
+				})
+				.catch(err => {
+					console.error(err);
+					reject(err);
+				});
+		})
+	}
 
-	passwordValid: (password) => {
+	passwordValid(password) {
 		if (!isLength(password, 6, 200)) return false;
 		return regex.password.test(password);
-	},
-
-	_lockdown: () => {
-		lib.connection.close();
-		lockdown = true;
 	}
-};
-
-module.exports = lib;
+}

+ 87 - 91
backend/logic/discord.js

@@ -1,107 +1,103 @@
-let lockdown = false;
+const coreClass = require("../core");
 
+const EventEmitter = require('events');
 const Discord = require("discord.js");
-const logger = require("./logger");
 const config = require("config");
 
-const client = new Discord.Client();
+const bus = new EventEmitter();
 
-let messagesToSend = [];
+module.exports = class extends coreClass {
+	initialize() {
+		return new Promise((resolve, reject) => {
+			this.setStage(1);
 
-let connected = false;
+			this.client = new Discord.Client();
+			
+			this.connected = false;
+			this.adminAlertChannelId = config.get("apis.discord").loggingChannel;
+			
+			this.client.on("ready", () => {
+				this.logger.info("DISCORD_READY", `Logged in as ${this.client.user.tag}!`);
+				this.connected = true;
 
-// TODO Maybe we need to only finish init when ready is called, or maybe we don't wait for it
-module.exports = {
-  adminAlertChannelId: "",
+				//bus.emit("discordConnected");
 
-  init: function(discordToken, adminAlertChannelId, errorCb, cb) {
-    this.adminAlertChannelId = adminAlertChannelId;
+				resolve();
 
-    client.on("ready", () => {
-      logger.info("DISCORD_READY", `Logged in as ${client.user.tag}!`);
-      connected = true;
-      messagesToSend.forEach(message => {
-        this.sendAdminAlertMessage(message.message, message.color, message.type, message.critical, message.extraFields);
-      });
-      messagesToSend = [];
-    });
+				/*messagesToSend.forEach(message => {
+					this.sendAdminAlertMessage(message.message, message.color, message.type, message.critical, message.extraFields);
+				});
+				messagesToSend = [];*/
+			});
+		  
+			this.client.on("disconnect", () => {
+				this.logger.info("DISCORD_DISCONNECT", `Discord client was disconnected.`);
+				this.connected = false;
+			});
 
-    client.on("invalidated", () => {
-      logger.info("DISCORD_INVALIDATED", `Discord client was invalidated.`);
-      connected = false;
-    });
+			this.client.on("reconnecting", () => {
+				this.logger.info("DISCORD_RECONNECTING", `Discord client reconnecting.`);
+				this.connected = false;
+			});
+		
+			this.client.on("error", err => {
+				this.logger.info("DISCORD_ERROR", `Discord client encountered an error: ${err.message}.`);
 
-    client.on("disconnected", () => {
-      logger.info("DISCORD_DISCONNECTED", `Discord client was disconnected.`);
-      connected = false;
-    });
+				reject();
+			});
 
-    client.on("error", err => {
-      logger.info(
-        "DISCORD_ERROR",
-        `Discord client encountered an error: ${err.message}.`
-      );
-    });
+			this.client.login(config.get("apis.discord").token);
+		});
+	}
 
-    client.login(discordToken);
+	async sendAdminAlertMessage(message, color, type, critical, extraFields) {
+		try { await this._validateHook(); await this.connectedHook(); } catch { return; }
 
-    if (lockdown) return this._lockdown();
-    cb();
-  },
+		const channel = this.client.channels.find("id", this.adminAlertChannelId);
+		if (channel !== null) {
+			let richEmbed = new Discord.RichEmbed();
+			richEmbed.setAuthor(
+				"Musare Logger",
+				`${config.get("domain")}/favicon-194x194.png`,
+				config.get("domain")
+			);
+			richEmbed.setColor(color);
+			richEmbed.setDescription(message);
+			//richEmbed.setFooter("Footer", "https://musare.com/favicon-194x194.png");
+			//richEmbed.setImage("https://musare.com/favicon-194x194.png");
+			//richEmbed.setThumbnail("https://musare.com/favicon-194x194.png");
+			richEmbed.setTimestamp(new Date());
+			richEmbed.setTitle("MUSARE ALERT");
+			richEmbed.setURL(config.get("domain"));
+			richEmbed.addField("Type:", type, true);
+			richEmbed.addField("Critical:", critical ? "True" : "False", true);
+			extraFields.forEach(extraField => {
+				richEmbed.addField(
+					extraField.name,
+					extraField.value,
+					extraField.inline
+				);
+			});
 
-  sendAdminAlertMessage: function(message, color, type, critical, extraFields) {
-    if (!connected) return messagesToSend.push({message, color, type, critical, extraFields});
-    else {
-      let channel = client.channels.find("id", this.adminAlertChannelId);
-      if (channel !== null) {
-        let richEmbed = new Discord.RichEmbed();
-        richEmbed.setAuthor(
-          "Musare Logger",
-          config.get("domain") + "/favicon-194x194.png",
-          config.get("domain")
-        );
-        richEmbed.setColor(color);
-        richEmbed.setDescription(message);
-        //richEmbed.setFooter("Footer", "https://musare.com/favicon-194x194.png");
-        //richEmbed.setImage("https://musare.com/favicon-194x194.png");
-        //richEmbed.setThumbnail("https://musare.com/favicon-194x194.png");
-        richEmbed.setTimestamp(new Date());
-        richEmbed.setTitle("MUSARE ALERT");
-        richEmbed.setURL(config.get("domain"));
-        richEmbed.addField("Type:", type, true);
-        richEmbed.addField("Critical:", critical ? "True" : "False", true);
-        extraFields.forEach(extraField => {
-          richEmbed.addField(
-            extraField.name,
-            extraField.value,
-            extraField.inline
-          );
-        });
+			channel
+			.send(message, { embed: richEmbed })
+			.then(message =>
+				this.logger.success("SEND_ADMIN_ALERT_MESSAGE", `Sent admin alert message: ${message}`)
+			)
+			.catch(() =>
+				this.logger.error("SEND_ADMIN_ALERT_MESSAGE", "Couldn't send admin alert message")
+			);
+		} else {
+			this.logger.error("SEND_ADMIN_ALERT_MESSAGE", "Couldn't send admin alert message, channel was not found.");
+		}
+	}
 
-        channel
-          .send(message, { embed: richEmbed })
-          .then(message =>
-            logger.success(
-              "SEND_ADMIN_ALERT_MESSAGE",
-              `Sent admin alert message: ${message}`
-            )
-          )
-          .catch(() =>
-            logger.error(
-              "SEND_ADMIN_ALERT_MESSAGE",
-              "Couldn't send admin alert message"
-            )
-          );
-      } else {
-        logger.error(
-          "SEND_ADMIN_ALERT_MESSAGE",
-          "Couldn't send admin alert message, channel was not found."
-        );
-      }
-    }
-  },
-
-  _lockdown: () => {
-    lockdown = true;
-  }
-};
+	connectedHook() {
+		return Promise.race([
+			new Promise(resolve => bus.once("discordConnected", resolve)),
+			new Promise(resolve => {
+				if (this.connected) resolve();
+			})
+		]);
+	}
+}

+ 147 - 138
backend/logic/io.js

@@ -2,165 +2,174 @@
 
 // This file contains all the logic for Socket.IO
 
-const app = require('./app');
-const actions = require('./actions');
-const async = require('async');
-const cache = require('./cache');
-const utils = require('./utils');
-const db = require('./db');
-const logger = require('./logger');
-const punishments = require('./punishments');
+const coreClass = require("../core");
 
-let initialized = false;
-let lockdown = false;
+const socketio = require("socket.io");
+const async = require("async");
 
-module.exports = {
+module.exports = class extends coreClass {
+	constructor(name, moduleManager) {
+		super(name, moduleManager);
 
-	io: null,
+		this.dependsOn = ["app", "db", "cache"];
+	}
 
-	init: (cb) => {
-		//TODO Check every 30s/60s, for all sockets, if they are still allowed to be in the rooms they are in, and on socket at all (permission changing/banning)
-		this.io = require('socket.io')(app.server);
+	initialize() {
+		return new Promise((resolve, reject) => {
+			this.setStage(1);
 
-		this.io.use((socket, next) => {
-			if (lockdown) return;
-			let cookies = socket.request.headers.cookie;
-			let SID = utils.cookies.parseCookies(cookies).SID;
+			const 	logger		= this.logger,
+					app			= this.moduleManager.modules["app"],
+					cache		= this.moduleManager.modules["cache"],
+					utils		= this.moduleManager.modules["utils"],
+					db			= this.moduleManager.modules["db"],
+					punishments	= this.moduleManager.modules["punishments"];
+			
+			const actions = require('../logic/actions');
 
-			socket.ip = socket.request.headers['x-forwarded-for'] || '0.0.0.0';
+			//TODO Check every 30s/60s, for all sockets, if they are still allowed to be in the rooms they are in, and on socket at all (permission changing/banning)
+			this.io = socketio(app.server);
 
-			async.waterfall([
-				(next) => {
-					if (!SID) return next('No SID.');
-					next();
-				},
-				(next) => {
-					cache.hget('sessions', SID, next);
-				},
-				(session, next) => {
-					if (!session) return next('No session found.');
-					session.refreshDate = Date.now();
-					socket.session = session;
-					cache.hset('sessions', SID, session, next);
-				},
-				(res, next) => {
-					punishments.getPunishments((err, punishments) => {
-						const isLoggedIn = !!(socket.session && socket.session.refreshDate);
-						const userId = (isLoggedIn) ? socket.session.userId : null;
-						let ban = 0;
-						let banned = false;
-						punishments.forEach(punishment => {
-							if (punishment.expiresAt > ban) ban = punishment;
-							if (punishment.type === 'banUserId' && isLoggedIn && punishment.value === userId) banned = true;
-							if (punishment.type === 'banUserIp' && punishment.value === socket.ip) banned = true;
+			this.io.use(async (socket, next) => {
+				try { await this._validateHook(); } catch { return; }
+
+				let cookies = socket.request.headers.cookie;
+				let SID;
+
+				socket.ip = socket.request.headers['x-forwarded-for'] || '0.0.0.0';
+
+				async.waterfall([
+					(next) => {
+						utils.parseCookies(cookies).then((parsedCookies) => {
+							SID = parsedCookies.SID;
+							next(null);
 						});
-						socket.banned = banned;
-						socket.ban = ban;
+					},
+
+					(next) => {
+						if (!SID) return next('No SID.');
 						next();
-					});
-				}
-			], () => {
-				if (!socket.session) {
-					socket.session = { socketId: socket.id };
-				} else socket.session.socketId = socket.id;
-				next();
+					},
+					(next) => {
+						cache.hget('sessions', SID, next);
+					},
+					(session, next) => {
+						if (!session) return next('No session found.');
+						session.refreshDate = Date.now();
+						socket.session = session;
+						cache.hset('sessions', SID, session, next);
+					},
+					(res, next) => {
+						punishments.getPunishments((err, punishments) => {
+							const isLoggedIn = !!(socket.session && socket.session.refreshDate);
+							const userId = (isLoggedIn) ? socket.session.userId : null;
+							let ban = 0;
+							let banned = false;
+							punishments.forEach(punishment => {
+								if (punishment.expiresAt > ban) ban = punishment;
+								if (punishment.type === 'banUserId' && isLoggedIn && punishment.value === userId) banned = true;
+								if (punishment.type === 'banUserIp' && punishment.value === socket.ip) banned = true;
+							});
+							socket.banned = banned;
+							socket.ban = ban;
+							next();
+						});
+					}
+				], () => {
+					if (!socket.session) {
+						socket.session = { socketId: socket.id };
+					} else socket.session.socketId = socket.id;
+					next();
+				});
 			});
-		});
 
-		this.io.on('connection', socket => {
-			if (lockdown) return socket.disconnect(true);
-			let sessionInfo = '';
-			if (socket.session.sessionId) sessionInfo = ` UserID: ${socket.session.userId}.`;
-			if (socket.banned) {
-				logger.info('IO_BANNED_CONNECTION', `A user tried to connect, but is currently banned. IP: ${socket.ip}.${sessionInfo}`);
-				socket.emit('keep.event:banned', socket.ban);
-				socket.disconnect(true);
-			} else {
-				logger.info('IO_CONNECTION', `User connected. IP: ${socket.ip}.${sessionInfo}`);
-
-				// catch when the socket has been disconnected
-				socket.on('disconnect', (reason) => {
-					let sessionInfo = '';
-					if (socket.session.sessionId) sessionInfo = ` UserID: ${socket.session.userId}.`;
-					logger.info('IO_DISCONNECTION', `User disconnected. IP: ${socket.ip}.${sessionInfo}`);
-				});
+			this.io.on('connection', async socket => {
+				try { await this._validateHook(); } catch { return; }
+
+				let sessionInfo = '';
+				if (socket.session.sessionId) sessionInfo = ` UserID: ${socket.session.userId}.`;
+				if (socket.banned) {
+					logger.info('IO_BANNED_CONNECTION', `A user tried to connect, but is currently banned. IP: ${socket.ip}.${sessionInfo}`);
+					socket.emit('keep.event:banned', socket.ban);
+					socket.disconnect(true);
+				} else {
+					logger.info('IO_CONNECTION', `User connected. IP: ${socket.ip}.${sessionInfo}`);
+
+					// catch when the socket has been disconnected
+					socket.on('disconnect', (reason) => {
+						let sessionInfo = '';
+						if (socket.session.sessionId) sessionInfo = ` UserID: ${socket.session.userId}.`;
+						logger.info('IO_DISCONNECTION', `User disconnected. IP: ${socket.ip}.${sessionInfo}`);
+					});
 
-				// catch errors on the socket (internal to socket.io)
-				socket.on('error', err => console.error(err));
+					// catch errors on the socket (internal to socket.io)
+					socket.on('error', err => console.error(err));
 
-				// have the socket listen for each action
-				Object.keys(actions).forEach((namespace) => {
-					Object.keys(actions[namespace]).forEach((action) => {
+					// have the socket listen for each action
+					Object.keys(actions).forEach((namespace) => {
+						Object.keys(actions[namespace]).forEach((action) => {
 
-						// the full name of the action
-						let name = `${namespace}.${action}`;
+							// the full name of the action
+							let name = `${namespace}.${action}`;
 
-						// listen for this action to be called
-						socket.on(name, function () {
-							let args = Array.prototype.slice.call(arguments, 0, -1);
-							let cb = arguments[arguments.length - 1];
+							// listen for this action to be called
+							socket.on(name, async (...args) => {
+								let cb = args[args.length - 1];
+								if (typeof cb !== "function")
+									cb = () => {
+										this.logger.info("IO_MODULE", `There was no callback provided for ${name}.`);
+									}
+								else args.pop();
 
-							if (lockdown) return cb({status: 'failure', message: 'Lockdown'});
+								try { await this._validateHook(); } catch { return cb({status: 'failure', message: 'Lockdown'}); } 
 
-							// load the session from the cache
-							cache.hget('sessions', socket.session.sessionId, (err, session) => {
-								if (err && err !== true) {
-									if (typeof cb === 'function') return cb({
-										status: 'error',
-										message: 'An error occurred while obtaining your session'
-									});
-								}
+								// load the session from the cache
+								cache.hget('sessions', socket.session.sessionId, (err, session) => {
+									if (err && err !== true) {
+										if (typeof cb === 'function') return cb({
+											status: 'error',
+											message: 'An error occurred while obtaining your session'
+										});
+									}
 
-								// make sure the sockets sessionId isn't set if there is no session
-								if (socket.session.sessionId && session === null) delete socket.session.sessionId;
+									// make sure the sockets sessionId isn't set if there is no session
+									if (socket.session.sessionId && session === null) delete socket.session.sessionId;
+
+									// call the action, passing it the session, and the arguments socket.io passed us
+									actions[namespace][action].apply(null, [socket.session].concat(args).concat([
+										(result) => {
+											// respond to the socket with our message
+											if (typeof cb === 'function') return cb(result);
+										}
+									]));
+								});
+							})
+						})
+					});
 
-								// call the action, passing it the session, and the arguments socket.io passed us
-								actions[namespace][action].apply(null, [socket.session].concat(args).concat([
-									(result) => {
-										// respond to the socket with our message
-										if (typeof cb === 'function') return cb(result);
+					if (socket.session.sessionId) {
+						cache.hget('sessions', socket.session.sessionId, (err, session) => {
+							if (err && err !== true) socket.emit('ready', false);
+							else if (session && session.userId) {
+								db.models.user.findOne({ _id: session.userId }, (err, user) => {
+									if (err || !user) return socket.emit('ready', false);
+									let role = '';
+									let username = '';
+									let userId = '';
+									if (user) {
+										role = user.role;
+										username = user.username;
+										userId = session.userId;
 									}
-								]));
-							});
+									socket.emit('ready', true, role, username, userId);
+								});
+							} else socket.emit('ready', false);
 						})
-					})
-				});
+					} else socket.emit('ready', false);
+				}
+			});
 
-				if (socket.session.sessionId) {
-					cache.hget('sessions', socket.session.sessionId, (err, session) => {
-						if (err && err !== true) socket.emit('ready', false);
-						else if (session && session.userId) {
-							db.models.user.findOne({ _id: session.userId }, (err, user) => {
-								if (err || !user) return socket.emit('ready', false);
-								let role = '';
-								let username = '';
-								let userId = '';
-								if (user) {
-									role = user.role;
-									username = user.username;
-									userId = session.userId;
-								}
-								socket.emit('ready', true, role, username, userId);
-							});
-						} else socket.emit('ready', false);
-					})
-				} else socket.emit('ready', false);
-			}
+			resolve();
 		});
-
-		initialized = true;
-
-		if (lockdown) return this._lockdown();
-		cb();
-	},
-
-	_lockdown: () => {
-		this.io.close();
-		let connected = this.io.of('/').connected;
-		for (let key in connected) {
-			connected[key].disconnect('Lockdown');
-		}
-		lockdown = true;
 	}
-
-};
+}

+ 153 - 178
backend/logic/logger.js

@@ -1,78 +1,15 @@
 'use strict';
 
-const dir = `${__dirname}/../../log`;
-const fs = require('fs');
-const config = require('config');
-//const Discord = require("discord.js");
-let client;
-let utils;
-
-if (!config.isDocker && !fs.existsSync(`${dir}`)) {
-	fs.mkdirSync(dir);
-}
-
-let started;
-let success = 0;
-let successThisMinute = 0;
-let successThisHour = 0;
-let error = 0;
-let errorThisMinute = 0;
-let errorThisHour = 0;
-let info = 0;
-let infoThisMinute = 0;
-let infoThisHour = 0;
-
-let successUnitsPerMinute = [0,0,0,0,0,0,0,0,0,0];
-let errorUnitsPerMinute = [0,0,0,0,0,0,0,0,0,0];
-let infoUnitsPerMinute = [0,0,0,0,0,0,0,0,0,0];
-
-let successUnitsPerHour = [0,0,0,0,0,0,0,0,0,0];
-let errorUnitsPerHour = [0,0,0,0,0,0,0,0,0,0];
-let infoUnitsPerHour = [0,0,0,0,0,0,0,0,0,0];
-
-function calculateUnits(units, unit) {
-	units.push(unit);
-	if (units.length > 10) units.shift();
-	return units;
-}
-
-function calculateHourUnits() {
-	successUnitsPerHour = calculateUnits(successUnitsPerHour, successThisHour);
-	errorUnitsPerHour = calculateUnits(errorUnitsPerHour, errorThisHour);
-	infoUnitsPerHour = calculateUnits(infoUnitsPerHour, infoThisHour);
+const coreClass = require("../core");
 
-	successThisHour = 0;
-	errorThisHour = 0;
-	infoThisHour = 0;
-
-	utils.emitToRoom('admin.statistics', 'event:admin.statistics.success.units.hour', successUnitsPerHour);
-	utils.emitToRoom('admin.statistics', 'event:admin.statistics.error.units.hour', errorUnitsPerHour);
-	utils.emitToRoom('admin.statistics', 'event:admin.statistics.info.units.hour', infoUnitsPerHour);
-
-	setTimeout(calculateHourUnits, 1000 * 60 * 60)
-}
-
-function calculateMinuteUnits() {
-	successUnitsPerMinute = calculateUnits(successUnitsPerMinute, successThisMinute);
-	errorUnitsPerMinute = calculateUnits(errorUnitsPerMinute, errorThisMinute);
-	infoUnitsPerMinute = calculateUnits(infoUnitsPerMinute, infoThisMinute);
-
-	successThisMinute = 0;
-	errorThisMinute = 0;
-	infoThisMinute = 0;
-
-	utils.emitToRoom('admin.statistics', 'event:admin.statistics.success.units.minute', successUnitsPerMinute);
-	utils.emitToRoom('admin.statistics', 'event:admin.statistics.error.units.minute', errorUnitsPerMinute);
-	utils.emitToRoom('admin.statistics', 'event:admin.statistics.info.units.minute', infoUnitsPerMinute);
-	
-	setTimeout(calculateMinuteUnits, 1000 * 60)
-}
+const config = require('config');
+const fs = require('fs');
 
-let twoDigits = (num) => {
+const twoDigits = (num) => {
 	return (num < 10) ? '0' + num : num;
 };
 
-let getTime = () => {
+const getTime = () => {
 	let time = new Date();
 	return {
 		year: time.getFullYear(),
@@ -84,121 +21,159 @@ let getTime = () => {
 	}
 };
 
-let getTimeFormatted = () => {
+const getTimeFormatted = () => {
 	let time = getTime();
 	return `${time.year}-${twoDigits(time.month)}-${twoDigits(time.day)} ${twoDigits(time.hour)}:${twoDigits(time.minute)}:${twoDigits(time.second)}`;
 }
 
-let initialized = false;
-let lockdown = false;
-
-module.exports = {
-	init: function(cb) {
-		utils = require('./utils');
-		started = Date.now();
-
-		setTimeout(calculateMinuteUnits, 1000 * 60);
-		setTimeout(calculateHourUnits, 1000 * 60 * 60);
-		setTimeout(this.calculate, 1000 * 30);
-
-		let time = getTimeFormatted();
-		fs.appendFile(dir + '/all.log', `${time} BACKEND_RESTARTED\n`, ()=>{});
-		fs.appendFile(dir + '/success.log', `${time} BACKEND_RESTARTED\n`, ()=>{});
-		fs.appendFile(dir + '/error.log', `${time} BACKEND_RESTARTED\n`, ()=>{});
-		fs.appendFile(dir + '/info.log', `${time} BACKEND_RESTARTED\n`, ()=>{});
-		fs.appendFile(dir + '/debugStation.log', `${time} BACKEND_RESTARTED\n`, ()=>{});
-
-		initialized = true;
-
-		if (lockdown) return this._lockdown();
-		cb();
-	},
-	success: (type, message, display = true) => {
-		if (lockdown) return;
-		success++;
-		successThisMinute++;
-		successThisHour++;
-		let time = getTimeFormatted();
-		fs.appendFile(dir + '/all.log', `${time} SUCCESS - ${type} - ${message}\n`, ()=>{});
-		fs.appendFile(dir + '/success.log', `${time} SUCCESS - ${type} - ${message}\n`, ()=>{});
-		if (display) console.info('\x1b[32m', time, 'SUCCESS', '-', type, '-', message, '\x1b[0m');
-	},
-	error: (type, message, display = true) => {
-		if (lockdown) return;
-		error++;
-		errorThisMinute++;
-		errorThisHour++;
-		let time = getTimeFormatted();
-		fs.appendFile(dir + '/all.log', `${time} ERROR - ${type} - ${message}\n`, ()=>{});
-		fs.appendFile(dir + '/error.log', `${time} ERROR - ${type} - ${message}\n`, ()=>{});
-		if (display) console.warn('\x1b[31m', time, 'ERROR', '-', type, '-', message, '\x1b[0m');
-	},
-	info: (type, message, display = true) => {
-		if (lockdown) return;
-		info++;
-		infoThisMinute++;
-		infoThisHour++;
-		let time = getTimeFormatted();
-		fs.appendFile(dir + '/all.log', `${time} INFO - ${type} - ${message}\n`, ()=>{});
-		fs.appendFile(dir + '/info.log', `${time} INFO - ${type} - ${message}\n`, ()=>{});
-		if (display) console.info('\x1b[36m', time, 'INFO', '-', type, '-', message, '\x1b[0m');
-	},
-	stationIssue: (string, display = false) => {
-		if (lockdown) return;
-		let time = getTimeFormatted();
-		fs.appendFile(dir + '/debugStation.log', `${time} - ${string}\n`, ()=>{});
-		if (display) console.info('\x1b[35m', time, '-', string, '\x1b[0m');
-	},
-	calculatePerSecond: function(number) {
-		if (lockdown) return;
-		let secondsRunning = Math.floor((Date.now() - started) / 1000);
-		let perSecond = number / secondsRunning;
-		return perSecond;
-	},
-	calculatePerMinute: function(number) {
-		if (lockdown) return;
-		let perMinute = this.calculatePerSecond(number) * 60;
-		return perMinute;
-	},
-	calculatePerHour: function(number) {
-		if (lockdown) return;
-		let perHour = this.calculatePerMinute(number) * 60;
-		return perHour;
-	},
-	calculatePerDay: function(number) {
-		if (lockdown) return;
-		let perDay = this.calculatePerHour(number) * 24;
-		return perDay;
-	},
-	calculate: function() {
-		if (lockdown) return;
-		let _this = module.exports;
-		utils.emitToRoom('admin.statistics', 'event:admin.statistics.logs', {
-			second: {
-				success: _this.calculatePerSecond(success),
-				error: _this.calculatePerSecond(error),
-				info: _this.calculatePerSecond(info)
-			},
-			minute: {
-				success: _this.calculatePerMinute(success),
-				error: _this.calculatePerMinute(error),
-				info: _this.calculatePerMinute(info)
-			},
-			hour: {
-				success: _this.calculatePerHour(success),
-				error: _this.calculatePerHour(error),
-				info: _this.calculatePerHour(info)
-			},
-			day: {
-				success: _this.calculatePerDay(success),
-				error: _this.calculatePerDay(error),
-				info: _this.calculatePerDay(info)
+module.exports = class extends coreClass {
+	initialize() {
+		return new Promise((resolve, reject) => {
+			this.setStage(1);
+
+			this.configDirectory = `${__dirname}/../../log`;
+
+			if (!config.isDocker && !fs.existsSync(`${this.configDirectory}`))
+				fs.mkdirSync(this.configDirectory);
+
+			let time = getTimeFormatted();
+
+			this.logCbs = [];
+
+			this.colors = {
+				Reset: "\x1b[0m",
+				Bright: "\x1b[1m",
+				Dim: "\x1b[2m",
+				Underscore: "\x1b[4m",
+				Blink: "\x1b[5m",
+				Reverse: "\x1b[7m",
+				Hidden: "\x1b[8m",
+
+				FgBlack: "\x1b[30m",
+				FgRed: "\x1b[31m",
+				FgGreen: "\x1b[32m",
+				FgYellow: "\x1b[33m",
+				FgBlue: "\x1b[34m",
+				FgMagenta: "\x1b[35m",
+				FgCyan: "\x1b[36m",
+				FgWhite: "\x1b[37m",
+
+				BgBlack: "\x1b[40m",
+				BgRed: "\x1b[41m",
+				BgGreen: "\x1b[42m",
+				BgYellow: "\x1b[43m",
+				BgBlue: "\x1b[44m",
+				BgMagenta: "\x1b[45m",
+				BgCyan: "\x1b[46m",
+				BgWhite: "\x1b[47m"
+			};
+
+			fs.appendFile(this.configDirectory + '/all.log', `${time} BACKEND_RESTARTED\n`, ()=>{});
+			fs.appendFile(this.configDirectory + '/success.log', `${time} BACKEND_RESTARTED\n`, ()=>{});
+			fs.appendFile(this.configDirectory + '/error.log', `${time} BACKEND_RESTARTED\n`, ()=>{});
+			fs.appendFile(this.configDirectory + '/info.log', `${time} BACKEND_RESTARTED\n`, ()=>{});
+			fs.appendFile(this.configDirectory + '/debugStation.log', `${time} BACKEND_RESTARTED\n`, ()=>{});
+
+			for(let i = 0; i < this.reservedLines; i++) {
+				process.stdout.write("\n");
 			}
+
+			resolve();
 		});
-		setTimeout(_this.calculate, 1000 * 30);
-	},
+	}
+
+	async success(type, text, display = true) {
+		try { await this._validateHook(); } catch { return; }
+
+		const time = getTimeFormatted();
+		const message = `${time} SUCCESS - ${type} - ${text}`;
+
+		this.writeFile('all.log', message);
+		this.writeFile('success.log', message);
 
-	_lockdown: () => {
-		lockdown = true;
+		if (display) this.log(this.colors.FgGreen, message);
 	}
-};
+
+	async error(type, text, display = true) {
+		try { await this._validateHook(); } catch { return; }
+
+		const time = getTimeFormatted();
+		const message = `${time} ERROR - ${type} - ${text}`;
+
+		this.writeFile('all.log', message);
+		this.writeFile('error.log', message);
+
+		if (display) this.log(this.colors.FgRed, message);
+	}
+
+	async info(type, text, display = true) {
+		try { await this._validateHook(); } catch { return; }
+
+		const time = getTimeFormatted();
+		const message = `${time} INFO - ${type} - ${text}`;
+
+		this.writeFile('all.log', message);
+		this.writeFile('info.log', message);
+		if (display) this.log(this.colors.FgCyan, message);
+	}
+
+	async debug(text, display = true) {
+		try { await this._validateHook(); } catch { return; }
+
+		const time = getTimeFormatted();
+		const message = `${time} DEBUG - ${text}`;
+
+		if (display) this.log(this.colors.FgMagenta, message);
+	}
+
+	async stationIssue(text, display = false) {
+		try { await this._validateHook(); } catch { return; }
+
+		const time = getTimeFormatted();
+		const message = `${time} DEBUG_STATION - ${text}`;
+
+		this.writeFile('debugStation.log', message);
+
+		if (display) this.log(this.colors.FgMagenta, message);
+	}
+
+	log(color, message) {
+		this.logCbs.push(() => {
+			this.logCbs.shift();
+			this.logActive = true;
+
+			const rows = process.stdout.rows;
+			const columns = process.stdout.columns;
+			const lineNumber = rows - this.reservedLines;
+
+			let lines = Math.floor(message.length / columns) + 1;
+
+			process.stdout.cursorTo(0, lineNumber);
+			process.stdout.write(`${color}${message}${this.colors.Reset}\n`);
+
+			process.stdout.cursorTo(0, (rows - this.logger.reservedLines) + lines);
+			process.stdout.clearScreenDown();
+
+			process.stdout.cursorTo(0, process.stdout.rows);
+			for(let i = 0; i < lines; i++) {
+				process.stdout.write(`\n`);
+			}
+
+			this.moduleManager.printStatus();
+
+			this.logActive = false;
+			this.nextLog();
+		});
+		this.nextLog();
+	}
+
+	nextLog() {
+		if (!this.logActive && this.logCbs.length > 0) {
+			this.logCbs[0]();
+		}
+	}
+
+	writeFile(fileName, message) {
+		fs.appendFile(`${this.configDirectory}/${fileName}`, `${message}\n`, ()=>{});
+	}
+}

+ 30 - 35
backend/logic/mail/index.js

@@ -1,45 +1,40 @@
 'use strict';
 
-const config = require('config');
-const enabled = config.get('apis.mailgun.enabled');
-let mailgun = null;
-if (enabled) {
-	mailgun = require('mailgun-js')({
-		apiKey: config.get("apis.mailgun.key"),
-		domain: config.get("apis.mailgun.domain")
-	});
-}
+const coreClass = require("../../core");
 
-let initialized = false;
-let lockdown = false;
-
-let lib = {
-
-	schemas: {},
+const config = require('config');
 
-	init: (cb) => {
-		lib.schemas = {
-			verifyEmail: require('./schemas/verifyEmail'),
-			resetPasswordRequest: require('./schemas/resetPasswordRequest'),
-			passwordRequest: require('./schemas/passwordRequest')
-		};
+let mailgun = null;
 
-		initialized = true;
+module.exports = class extends coreClass {
+	initialize() {
+		return new Promise((resolve, reject) => {
+			this.setStage(1);
+
+			this.schemas = {
+				verifyEmail: require('./schemas/verifyEmail'),
+				resetPasswordRequest: require('./schemas/resetPasswordRequest'),
+				passwordRequest: require('./schemas/passwordRequest')
+			};
+
+			this.enabled = config.get('apis.mailgun.enabled');
+
+			if (this.enabled)
+				mailgun = require('mailgun-js')({
+					apiKey: config.get("apis.mailgun.key"),
+					domain: config.get("apis.mailgun.domain")
+				});
+			
+			resolve();
+		});
+	}
 
-		if (lockdown) return this._lockdown();
-		cb();
-	},
+	async sendMail(data, cb) {
+		try { await this._validateHook(); } catch { return; }
 
-	sendMail: (data, cb) => {
-		if (lockdown) return cb('Lockdown');
 		if (!cb) cb = ()=>{};
-		if (enabled) mailgun.messages().send(data, cb);
-		else cb();
-	},
 
-	_lockdown: () => {
-		lockdown = true;
+		if (this.enabled) mailgun.messages().send(data, cb);
+		else cb();
 	}
-};
-
-module.exports = lib;
+}

+ 4 - 1
backend/logic/mail/schemas/passwordRequest.js

@@ -1,5 +1,8 @@
 const config = require('config');
-const mail = require('../index');
+
+const moduleManager = require('../../../index');
+
+const mail = moduleManager.modules["mail"];
 
 /**
  * Sends a request password email

+ 4 - 1
backend/logic/mail/schemas/resetPasswordRequest.js

@@ -1,5 +1,8 @@
 const config = require('config');
-const mail = require('../index');
+
+const moduleManager = require('../../../index');
+
+const mail = moduleManager.modules["mail"];
 
 /**
  * Sends a request password reset email

+ 4 - 1
backend/logic/mail/schemas/verifyEmail.js

@@ -1,5 +1,8 @@
 const config = require('config');
-const mail = require('../index');
+
+const moduleManager = require('../../../index');
+
+const mail = moduleManager.modules["mail"];
 
 /**
  * Sends a verify email email

+ 50 - 58
backend/logic/notifications.js

@@ -1,49 +1,44 @@
 'use strict';
 
+const coreClass = require("../core");
+
 const crypto = require('crypto');
 const redis = require('redis');
-const logger = require('./logger');
+const config = require('config');
 
 const subscriptions = [];
 
-let initialized = false;
-let lockdown = false;
-let errorCb;
+module.exports = class extends coreClass {
+	initialize() {
+		return new Promise((resolve, reject) => {
+			this.setStage(1);
 
-const lib = {
+			const url = this.url = config.get("redis").url;
+			const password = this.password = config.get("redis").password;
 
-	pub: null,
-	sub: null,
-	errorCb: null,
+			this.pub = redis.createClient({ url, password });
+			this.sub = redis.createClient({ url, password });
 
-	/**
-	 * Initializes the notifications module
-	 *
-	 * @param {String} url - the url of the redis server
-	 * @param {String} password - the password of the redis server
-	 * @param {Function} cb - gets called once we're done initializing
-	 */
-	init: (url, password, errorCb, cb) => {
-		lib.errorCb = errorCb;
-		lib.pub = redis.createClient({ url, password });
-		lib.sub = redis.createClient({ url, password });
-		lib.sub.on('error', (err) => {
-			errorCb('Cache connection error.', err, 'Notifications');
-		});
-		lib.sub.on('pmessage', (pattern, channel, expiredKey) => {
-			logger.stationIssue(`PMESSAGE - Pattern: ${pattern}; Channel: ${channel}; ExpiredKey: ${expiredKey}`);
-			subscriptions.forEach((sub) => {
-				if (sub.name !== expiredKey) return;
-				sub.cb();
+			this.sub.on('error', (err) => {
+				errorCb('Cache connection error.', err, 'Notifications');
+				reject(err);
 			});
-		});
-		lib.sub.psubscribe('__keyevent@0__:expired');
 
-		initialized = true;
+			this.sub.on("connect", () => {
+				resolve();
+			});
 
-		if (lockdown) return this._lockdown();
-		cb();
-	},
+			this.sub.on('pmessage', (pattern, channel, expiredKey) => {
+				this.logger.stationIssue(`PMESSAGE - Pattern: ${pattern}; Channel: ${channel}; ExpiredKey: ${expiredKey}`);
+				subscriptions.forEach((sub) => {
+					if (sub.name !== expiredKey) return;
+					sub.cb();
+				});
+			});
+
+			this.sub.psubscribe('__keyevent@0__:expired');
+		});
+	}
 
 	/**
 	 * Schedules a notification to be dispatched in a specific amount of milliseconds,
@@ -54,13 +49,15 @@ const lib = {
 	 * @param {Integer} time - how long in milliseconds until the notification should be fired
 	 * @param {Function} cb - gets called when the notification has been scheduled
 	 */
-	schedule: (name, time, cb, station) => {
-		if (lockdown) return;
+	async schedule(name, time, cb, station) {
+		try { await this._validateHook(); } catch { return; }
+
 		if (!cb) cb = ()=>{};
+
 		time = Math.round(time);
-		logger.stationIssue(`SCHEDULE - Time: ${time}; Name: ${name}; Key: ${crypto.createHash('md5').update(`_notification:${name}_`).digest('hex')}; StationId: ${station._id}; StationName: ${station.name}`);
-		lib.pub.set(crypto.createHash('md5').update(`_notification:${name}_`).digest('hex'), '', 'PX', time, 'NX', cb);
-	},
+		this.logger.stationIssue(`SCHEDULE - Time: ${time}; Name: ${name}; Key: ${crypto.createHash('md5').update(`_notification:${name}_`).digest('hex')}; StationId: ${station._id}; StationName: ${station.name}`);
+		this.pub.set(crypto.createHash('md5').update(`_notification:${name}_`).digest('hex'), '', 'PX', time, 'NX', cb);
+	}
 
 	/**
 	 * Subscribes a callback function to be called when a notification gets called
@@ -70,37 +67,32 @@ const lib = {
 	 * @param {Boolean} unique - only subscribe if another subscription with the same name doesn't already exist
 	 * @return {Object} - the subscription object
 	 */
-	subscribe: (name, cb, unique = false, station) => {
-		if (lockdown) return;
-		logger.stationIssue(`SUBSCRIBE - Name: ${name}; Key: ${crypto.createHash('md5').update(`_notification:${name}_`).digest('hex')}, StationId: ${station._id}; StationName: ${station.name}; Unique: ${unique}; SubscriptionExists: ${!!subscriptions.find((subscription) => subscription.originalName == name)};`);
+	async subscribe(name, cb, unique = false, station) {
+		try { await this._validateHook(); } catch { return; }
+
+		this.logger.stationIssue(`SUBSCRIBE - Name: ${name}; Key: ${crypto.createHash('md5').update(`_notification:${name}_`).digest('hex')}, StationId: ${station._id}; StationName: ${station.name}; Unique: ${unique}; SubscriptionExists: ${!!subscriptions.find((subscription) => subscription.originalName == name)};`);
 		if (unique && !!subscriptions.find((subscription) => subscription.originalName == name)) return;
 		let subscription = { originalName: name, name: crypto.createHash('md5').update(`_notification:${name}_`).digest('hex'), cb };
 		subscriptions.push(subscription);
 		return subscription;
-	},
+	}
 
 	/**
 	 * Remove a notification subscription
 	 *
 	 * @param {Object} subscription - the subscription object returned by {@link subscribe}
 	 */
-	remove: (subscription) => {
-		if (lockdown) return;
+	async remove(subscription) {
+		try { await this._validateHook(); } catch { return; }
+
 		let index = subscriptions.indexOf(subscription);
 		if (index) subscriptions.splice(index, 1);
-	},
-
-	unschedule: (name) => {
-		if (lockdown) return;
-		logger.stationIssue(`UNSCHEDULE - Name: ${name}; Key: ${crypto.createHash('md5').update(`_notification:${name}_`).digest('hex')}`);
-		lib.pub.del(crypto.createHash('md5').update(`_notification:${name}_`).digest('hex'));
-	},
-
-	_lockdown: () => {
-		lib.pub.quit();
-		lib.sub.quit();
-		lockdown = true;
 	}
-};
 
-module.exports = lib;
+	async unschedule(name) {
+		try { await this._validateHook(); } catch { return; }
+
+		this.logger.stationIssue(`UNSCHEDULE - Name: ${name}; Key: ${crypto.createHash('md5').update(`_notification:${name}_`).digest('hex')}`);
+		this.pub.del(crypto.createHash('md5').update(`_notification:${name}_`).digest('hex'));
+	}
+}

+ 79 - 74
backend/logic/playlists.js

@@ -1,59 +1,66 @@
 'use strict';
 
-const cache = require('./cache');
-const db = require('./db');
-const async = require('async');
-
-let initialized = false;
-let lockdown = false;
-
-module.exports = {
+const coreClass = require("../core");
 
-	/**
-	 * Initializes the playlists module, and exits if it is unsuccessful
-	 *
-	 * @param {Function} cb - gets called once we're done initializing
-	 */
-	init: cb => {
-		async.waterfall([
-			(next) => {
-				cache.hgetall('playlists', next);
-			},
+const async = require('async');
 
-			(playlists, next) => {
-				if (!playlists) return next();
-				let playlistIds = Object.keys(playlists);
-				async.each(playlistIds, (playlistId, next) => {
-					db.models.playlist.findOne({_id: playlistId}, (err, playlist) => {
-						if (err) next(err);
-						else if (!playlist) {
-							cache.hdel('playlists', playlistId, next);
-						}
-						else next();
-					});
-				}, next);
-			},
+module.exports = class extends coreClass {
+	constructor(name, moduleManager) {
+		super(name, moduleManager);
 
-			(next) => {
-				db.models.playlist.find({}, next);
-			},
+		this.dependsOn = ["cache", "db", "utils"];
+	}
 
-			(playlists, next) => {
-				async.each(playlists, (playlist, next) => {
-					cache.hset('playlists', playlist._id, cache.schemas.playlist(playlist), next);
-				}, next);
-			}
-		], (err) => {
-			if (lockdown) return this._lockdown();
-			if (err) {
-				err = utils.getError(err);
-				cb(err);
-			} else {
-				initialized = true;
-				cb();
-			}
+	initialize() {
+		return new Promise((resolve, reject) => {
+			this.setStage(1);
+
+			this.cache = this.moduleManager.modules["cache"];
+			this.db	= this.moduleManager.modules["db"];
+			this.utils	= this.moduleManager.modules["utils"];
+
+			async.waterfall([
+				(next) => {
+					this.setStage(2);
+					this.cache.hgetall('playlists', next);
+				},
+	
+				(playlists, next) => {
+					this.setStage(3);
+					if (!playlists) return next();
+					let playlistIds = Object.keys(playlists);
+					async.each(playlistIds, (playlistId, next) => {
+						this.db.models.playlist.findOne({_id: playlistId}, (err, playlist) => {
+							if (err) next(err);
+							else if (!playlist) {
+								this.cache.hdel('playlists', playlistId, next);
+							}
+							else next();
+						});
+					}, next);
+				},
+	
+				(next) => {
+					this.setStage(4);
+					this.db.models.playlist.find({}, next);
+				},
+	
+				(playlists, next) => {
+					this.setStage(5);
+					async.each(playlists, (playlist, next) => {
+						this.cache.hset('playlists', playlist._id, this.cache.schemas.playlist(playlist), next);
+					}, next);
+				}
+			], async (err) => {
+				if (err) {
+					err = await this.utils.getError(err);
+					reject(err);
+				} else {
+					resolve();
+				}
+			});
 		});
-	},
+	}
 
 	/**
 	 * Gets a playlist by id from the cache or Mongo, and if it isn't in the cache yet, adds it the cache
@@ -61,21 +68,22 @@ module.exports = {
 	 * @param {String} playlistId - the id of the playlist we are trying to get
 	 * @param {Function} cb - gets called once we're done initializing
 	 */
-	getPlaylist: (playlistId, cb) => {
-		if (lockdown) return cb('Lockdown');
+	async getPlaylist(playlistId, cb) {
+		try { await this._validateHook(); } catch { return; }
+
 		async.waterfall([
 			(next) => {
-				cache.hgetall('playlists', next);
+				this.cache.hgetall('playlists', next);
 			},
 
 			(playlists, next) => {
 				if (!playlists) return next();
 				let playlistIds = Object.keys(playlists);
 				async.each(playlistIds, (playlistId, next) => {
-					db.models.playlist.findOne({_id: playlistId}, (err, playlist) => {
+					this.db.models.playlist.findOne({_id: playlistId}, (err, playlist) => {
 						if (err) next(err);
 						else if (!playlist) {
-							cache.hdel('playlists', playlistId, next);
+							this.cache.hdel('playlists', playlistId, next);
 						}
 						else next();
 					});
@@ -83,17 +91,17 @@ module.exports = {
 			},
 
 			(next) => {
-				cache.hget('playlists', playlistId, next);
+				this.cache.hget('playlists', playlistId, next);
 			},
 
 			(playlist, next) => {
 				if (playlist) return next(true, playlist);
-				db.models.playlist.findOne({ _id: playlistId }, next);
+				this.db.models.playlist.findOne({ _id: playlistId }, next);
 			},
 
 			(playlist, next) => {
 				if (playlist) {
-					cache.hset('playlists', playlistId, playlist, next);
+					this.cache.hset('playlists', playlistId, playlist, next);
 				} else next('Playlist not found');
 			},
 
@@ -101,7 +109,7 @@ module.exports = {
 			if (err && err !== true) return cb(err);
 			else cb(null, playlist);
 		});
-	},
+	}
 
 	/**
 	 * Gets a playlist from id from Mongo and updates the cache with it
@@ -109,27 +117,27 @@ module.exports = {
 	 * @param {String} playlistId - the id of the playlist we are trying to update
 	 * @param {Function} cb - gets called when an error occurred or when the operation was successful
 	 */
-	updatePlaylist: (playlistId, cb) => {
-		if (lockdown) return cb('Lockdown');
-		async.waterfall([
+	async updatePlaylist(playlistId, cb) {
+		try { await this._validateHook(); } catch { return; }
 
+		async.waterfall([
 			(next) => {
-				db.models.playlist.findOne({ _id: playlistId }, next);
+				this.db.models.playlist.findOne({ _id: playlistId }, next);
 			},
 
 			(playlist, next) => {
 				if (!playlist) {
-					cache.hdel('playlists', playlistId);
+					this.cache.hdel('playlists', playlistId);
 					return next('Playlist not found');
 				}
-				cache.hset('playlists', playlistId, playlist, next);
+				this.cache.hset('playlists', playlistId, playlist, next);
 			}
 
 		], (err, playlist) => {
 			if (err && err !== true) return cb(err);
 			cb(null, playlist);
 		});
-	},
+	}
 
 	/**
 	 * Deletes playlist from id from Mongo and cache
@@ -137,16 +145,17 @@ module.exports = {
 	 * @param {String} playlistId - the id of the playlist we are trying to delete
 	 * @param {Function} cb - gets called when an error occurred or when the operation was successful
 	 */
-	deletePlaylist: (playlistId, cb) => {
-		if (lockdown) return cb('Lockdown');
+	async deletePlaylist(playlistId, cb) {
+		try { await this._validateHook(); } catch { return; }
+
 		async.waterfall([
 
 			(next) => {
-				db.models.playlist.deleteOne({ _id: playlistId }, next);
+				this.db.models.playlist.deleteOne({ _id: playlistId }, next);
 			},
 
 			(res, next) => {
-				cache.hdel('playlists', playlistId, next);
+				this.cache.hdel('playlists', playlistId, next);
 			}
 
 		], (err) => {
@@ -154,9 +163,5 @@ module.exports = {
 
 			cb(null);
 		});
-	},
-
-	_lockdown: () => {
-		lockdown = true;
 	}
-};
+}

+ 87 - 79
backend/logic/punishments.js

@@ -1,73 +1,80 @@
 'use strict';
 
-const cache = require('./cache');
-const db = require('./db');
-const io = require('./io');
-const utils = require('./utils');
+const coreClass = require("../core");
+
 const async = require('async');
 const mongoose = require('mongoose');
 
-let initialized = false;
-let lockdown = false;
-
-module.exports = {
-
-	/**
-	 * Initializes the punishments module, and exits if it is unsuccessful
-	 *
-	 * @param {Function} cb - gets called once we're done initializing
-	 */
-	init: cb => {
-		async.waterfall([
-			(next) => {
-				cache.hgetall('punishments', next);
-			},
-
-			(punishments, next) => {
-				if (!punishments) return next();
-				let punishmentIds = Object.keys(punishments);
-				async.each(punishmentIds, (punishmentId, next) => {
-					db.models.punishment.findOne({_id: punishmentId}, (err, punishment) => {
-						if (err) next(err);
-						else if (!punishment) cache.hdel('punishments', punishmentId, next);
-						else next();
-					});
-				}, next);
-			},
+module.exports = class extends coreClass {
+	constructor(name, moduleManager) {
+		super(name, moduleManager);
 
-			(next) => {
-				db.models.punishment.find({}, next);
-			},
+		this.dependsOn = ["cache", "db", "utils"];
+	}
 
-			(punishments, next) => {
-				async.each(punishments, (punishment, next) => {
-					if (punishment.active === false || punishment.expiresAt < Date.now()) return next();
-					cache.hset('punishments', punishment._id, cache.schemas.punishment(punishment, punishment._id), next);
-				}, next);
-			}
-		], (err) => {
-			if (lockdown) return this._lockdown();
-			if (err) {
-				err = utils.getError(err);
-				cb(err);
-			} else {
-				initialized = true;
-				cb();
-			}
+	initialize() {
+		return new Promise((resolve, reject) => {
+			this.setStage(1);
+
+			this.cache = this.moduleManager.modules['cache'];
+			this.db = this.moduleManager.modules['db'];
+			this.io = this.moduleManager.modules['io'];
+			this.utils = this.moduleManager.modules['utils'];
+
+			async.waterfall([
+				(next) => {
+					this.setStage(2);
+					this.cache.hgetall('punishments', next);
+				},
+	
+				(punishments, next) => {
+					this.setStage(3);
+					if (!punishments) return next();
+					let punishmentIds = Object.keys(punishments);
+					async.each(punishmentIds, (punishmentId, next) => {
+						this.db.models.punishment.findOne({_id: punishmentId}, (err, punishment) => {
+							if (err) next(err);
+							else if (!punishment) this.cache.hdel('punishments', punishmentId, next);
+							else next();
+						});
+					}, next);
+				},
+	
+				(next) => {
+					this.setStage(4);
+					this.db.models.punishment.find({}, next);
+				},
+	
+				(punishments, next) => {
+					this.setStage(5);
+					async.each(punishments, (punishment, next) => {
+						if (punishment.active === false || punishment.expiresAt < Date.now()) return next();
+						this.cache.hset('punishments', punishment._id, cache.schemas.punishment(punishment, punishment._id), next);
+					}, next);
+				}
+			], async (err) => {
+				if (err) {
+					err = await utils.getError(err);
+					reject(err);
+				} else {
+					resolve();
+				}
+			});
 		});
-	},
+	}
 
 	/**
 	 * Gets all punishments in the cache that are active, and removes those that have expired
 	 *
 	 * @param {Function} cb - gets called once we're done initializing
 	 */
-	getPunishments: function(cb) {
-		if (lockdown) return cb('Lockdown');
+	async getPunishments(cb) {
+		try { await this._validateHook(); } catch { return; }
+
 		let punishmentsToRemove = [];
 		async.waterfall([
 			(next) => {
-				cache.hgetall('punishments', next);
+				this.cache.hgetall('punishments', next);
 			},
 
 			(punishmentsObj, next) => {
@@ -88,7 +95,7 @@ module.exports = {
 				async.each(
 					punishmentsToRemove,
 					(punishment, next2) => {
-						cache.hdel('punishments', punishment.punishmentId, () => {
+						this.cache.hdel('punishments', punishment.punishmentId, () => {
 							next2();
 						});
 					},
@@ -102,7 +109,7 @@ module.exports = {
 
 			cb(null, punishments);
 		});
-	},
+	}
 
 	/**
 	 * Gets a punishment by id
@@ -110,23 +117,24 @@ module.exports = {
 	 * @param {String} id - the id of the punishment we are trying to get
 	 * @param {Function} cb - gets called once we're done initializing
 	 */
-	getPunishment: function(id, cb) {
-		if (lockdown) return cb('Lockdown');
+	async getPunishment(id, cb) {
+		try { await this._validateHook(); } catch { return; }
+
 		async.waterfall([
 
 			(next) => {
 				if (!mongoose.Types.ObjectId.isValid(id)) return next('Id is not a valid ObjectId.');
-				cache.hget('punishments', id, next);
+				this.cache.hget('punishments', id, next);
 			},
 
 			(punishment, next) => {
 				if (punishment) return next(true, punishment);
-				db.models.punishment.findOne({_id: id}, next);
+				this.db.models.punishment.findOne({_id: id}, next);
 			},
 
 			(punishment, next) => {
 				if (punishment) {
-					cache.hset('punishments', id, punishment, next);
+					this.cache.hset('punishments', id, punishment, next);
 				} else next('Punishment not found.');
 			},
 
@@ -135,7 +143,7 @@ module.exports = {
 
 			cb(null, punishment);
 		});
-	},
+	}
 
 	/**
 	 * Gets all punishments from a userId
@@ -143,11 +151,12 @@ module.exports = {
 	 * @param {String} userId - the userId of the punishment(s) we are trying to get
 	 * @param {Function} cb - gets called once we're done initializing
 	 */
-	getPunishmentsFromUserId: function(userId, cb) {
-		if (lockdown) return cb('Lockdown');
+	async getPunishmentsFromUserId(userId, cb) {
+		try { await this._validateHook(); } catch { return; }
+
 		async.waterfall([
 			(next) => {
-				module.exports.getPunishments(next);
+				this.getPunishments(next);
 			},
 			(punishments, next) => {
 				punishments = punishments.filter((punishment) => {
@@ -160,13 +169,14 @@ module.exports = {
 
 			cb(null, punishments);
 		});
-	},
+	}
+
+	async addPunishment(type, value, reason, expiresAt, punishedBy, cb) {
+		try { await this._validateHook(); } catch { return; }
 
-	addPunishment: function(type, value, reason, expiresAt, punishedBy, cb) {
-		if (lockdown) return cb('Lockdown');
 		async.waterfall([
 			(next) => {
-				const punishment = new db.models.punishment({
+				const punishment = new this.db.models.punishment({
 					type,
 					value,
 					reason,
@@ -182,7 +192,7 @@ module.exports = {
 			},
 
 			(punishment, next) => {
-				cache.hset('punishments', punishment._id, cache.schemas.punishment(punishment, punishment._id), (err) => {
+				this.cache.hset('punishments', punishment._id, this.cache.schemas.punishment(punishment, punishment._id), (err) => {
 					next(err, punishment);
 				});
 			},
@@ -194,13 +204,14 @@ module.exports = {
 		], (err, punishment) => {
 			cb(err, punishment);
 		});
-	},
+	}
+
+	async removePunishmentFromCache(punishmentId, cb) {
+		try { await this._validateHook(); } catch { return; }
 
-	removePunishmentFromCache: function(punishmentId, cb) {
-		if (lockdown) return cb('Lockdown');
 		async.waterfall([
 			(next) => {
-				const punishment = new db.models.punishment({
+				const punishment = new this.db.models.punishment({
 					type,
 					value,
 					reason,
@@ -217,7 +228,7 @@ module.exports = {
 			},
 
 			(punishment, next) => {
-				cache.hset('punishments', punishment._id, punishment, next);
+				this.cache.hset('punishments', punishment._id, punishment, next);
 			},
 
 			(punishment, next) => {
@@ -227,9 +238,6 @@ module.exports = {
 		], (err) => {
 			cb(err);
 		});
-	},
-
-	_lockdown: () => {
-		lockdown = true;
 	}
-};
+}
+

+ 80 - 72
backend/logic/songs.js

@@ -1,60 +1,69 @@
 'use strict';
 
-const cache = require('./cache');
-const db = require('./db');
-const io = require('./io');
-const utils = require('./utils');
+const coreClass = require("../core");
+
 const async = require('async');
 const mongoose = require('mongoose');
 
-let initialized = false;
-let lockdown = false;
 
-module.exports = {
 
-	/**
-	 * Initializes the songs module, and exits if it is unsuccessful
-	 *
-	 * @param {Function} cb - gets called once we're done initializing
-	 */
-	init: cb => {
-		async.waterfall([
-			(next) => {
-				cache.hgetall('songs', next);
-			},
 
-			(songs, next) => {
-				if (!songs) return next();
-				let songIds = Object.keys(songs);
-				async.each(songIds, (songId, next) => {
-					db.models.song.findOne({songId}, (err, song) => {
-						if (err) next(err);
-						else if (!song) cache.hdel('songs', songId, next);
-						else next();
-					});
-				}, next);
-			},
+module.exports = class extends coreClass {
+	constructor(name, moduleManager) {
+		super(name, moduleManager);
 
-			(next) => {
-				db.models.song.find({}, next);
-			},
+		this.dependsOn = ["utils", "cache", "db"];
+	}
 
-			(songs, next) => {
-				async.each(songs, (song, next) => {
-					cache.hset('songs', song.songId, cache.schemas.song(song), next);
-				}, next);
-			}
-		], (err) => {
-			if (lockdown) return this._lockdown();
-			if (err) {
-				err = utils.getError(err);
-				cb(err);
-			} else {
-				initialized = true;
-				cb();
-			}
+	initialize() {
+		return new Promise((resolve, reject) => {
+			this.setStage(1);
+
+			this.cache = this.moduleManager.modules["cache"];
+			this.db = this.moduleManager.modules["db"];
+			this.io = this.moduleManager.modules["io"];
+			this.utils = this.moduleManager.modules["utils"];
+
+			async.waterfall([
+				(next) => {
+					this.setStage(2);
+					this.cache.hgetall('songs', next);
+				},
+	
+				(songs, next) => {
+					this.setStage(3);
+					if (!songs) return next();
+					let songIds = Object.keys(songs);
+					async.each(songIds, (songId, next) => {
+						this.db.models.song.findOne({songId}, (err, song) => {
+							if (err) next(err);
+							else if (!song) this.cache.hdel('songs', songId, next);
+							else next();
+						});
+					}, next);
+				},
+	
+				(next) => {
+					this.setStage(4);
+					this.db.models.song.find({}, next);
+				},
+	
+				(songs, next) => {
+					this.setStage(5);
+					async.each(songs, (song, next) => {
+						this.cache.hset('songs', song.songId, this.cache.schemas.song(song), next);
+					}, next);
+				}
+			], async (err) => {
+				if (err) {
+					err = await this.utils.getError(err);
+					reject(err);
+				} else {
+					resolve();
+				}
+			});
 		});
-	},
+	}
 
 	/**
 	 * Gets a song by id from the cache or Mongo, and if it isn't in the cache yet, adds it the cache
@@ -62,23 +71,23 @@ module.exports = {
 	 * @param {String} id - the id of the song we are trying to get
 	 * @param {Function} cb - gets called once we're done initializing
 	 */
-	getSong: function(id, cb) {
-		if (lockdown) return cb('Lockdown');
-		async.waterfall([
+	async getSong(id, cb) {
+		try { await this._validateHook(); } catch { return; }
 
+		async.waterfall([
 			(next) => {
 				if (!mongoose.Types.ObjectId.isValid(id)) return next('Id is not a valid ObjectId.');
-				cache.hget('songs', id, next);
+				this.cache.hget('songs', id, next);
 			},
 
 			(song, next) => {
 				if (song) return next(true, song);
-				db.models.song.findOne({_id: id}, next);
+				this.db.models.song.findOne({_id: id}, next);
 			},
 
 			(song, next) => {
 				if (song) {
-					cache.hset('songs', id, song, next);
+					this.cache.hset('songs', id, song, next);
 				} else next('Song not found.');
 			},
 
@@ -87,7 +96,7 @@ module.exports = {
 
 			cb(null, song);
 		});
-	},
+	}
 
 	/**
 	 * Gets a song by song id from the cache or Mongo, and if it isn't in the cache yet, adds it the cache
@@ -95,17 +104,18 @@ module.exports = {
 	 * @param {String} songId - the mongo id of the song we are trying to get
 	 * @param {Function} cb - gets called once we're done initializing
 	 */
-	getSongFromId: function(songId, cb) {
-		if (lockdown) return cb('Lockdown');
+	async getSongFromId(songId, cb) {
+		try { await this._validateHook(); } catch { return; }
+
 		async.waterfall([
 			(next) => {
-				db.models.song.findOne({ songId }, next);
+				this.db.models.song.findOne({ songId }, next);
 			}
 		], (err, song) => {
 			if (err && err !== true) return cb(err);
 			else return cb(null, song);
 		});
-	},
+	}
 
 	/**
 	 * Gets a song from id from Mongo and updates the cache with it
@@ -113,21 +123,22 @@ module.exports = {
 	 * @param {String} songId - the id of the song we are trying to update
 	 * @param {Function} cb - gets called when an error occurred or when the operation was successful
 	 */
-	updateSong: (songId, cb) => {
-		if (lockdown) return cb('Lockdown');
+	async updateSong(songId, cb) {
+		try { await this._validateHook(); } catch { return; }
+
 		async.waterfall([
 
 			(next) => {
-				db.models.song.findOne({_id: songId}, next);
+				this.db.models.song.findOne({_id: songId}, next);
 			},
 
 			(song, next) => {
 				if (!song) {
-					cache.hdel('songs', songId);
+					this.cache.hdel('songs', songId);
 					return next('Song not found.');
 				}
 
-				cache.hset('songs', songId, song, next);
+				this.cache.hset('songs', songId, song, next);
 			}
 
 		], (err, song) => {
@@ -135,7 +146,7 @@ module.exports = {
 
 			cb(null, song);
 		});
-	},
+	}
 
 	/**
 	 * Deletes song from id from Mongo and cache
@@ -143,16 +154,17 @@ module.exports = {
 	 * @param {String} songId - the id of the song we are trying to delete
 	 * @param {Function} cb - gets called when an error occurred or when the operation was successful
 	 */
-	deleteSong: (songId, cb) => {
-		if (lockdown) return cb('Lockdown');
+	async deleteSong(songId, cb) {
+		try { await this._validateHook(); } catch { return; }
+
 		async.waterfall([
 
 			(next) => {
-				db.models.song.deleteOne({ songId }, next);
+				this.db.models.song.deleteOne({ songId }, next);
 			},
 
 			(next) => {
-				cache.hdel('songs', songId, next);
+				this.cache.hdel('songs', songId, next);
 			}
 
 		], (err) => {
@@ -160,9 +172,5 @@ module.exports = {
 
 			cb(null);
 		});
-	},
-
-	_lockdown: () => {
-		lockdown = true;
 	}
-};
+}

+ 61 - 50
backend/logic/spotify.js

@@ -1,19 +1,7 @@
-const config = require('config'),
-	  async  = require('async'),
-	  logger = require('./logger'),
-	  cache  = require('./cache');
-
-const client = config.get("apis.spotify.client");
-const secret = config.get("apis.spotify.secret");
+const coreClass = require("../core");
 
-const OAuth2 = require('oauth').OAuth2;
-const SpotifyOauth = new OAuth2(
-	client,
-	secret, 
-	'https://accounts.spotify.com/', 
-	null,
-	'api/token',
-	null);
+const config = require('config'),
+	async  = require('async');
 
 let apiResults = {
 	access_token: "",
@@ -23,45 +11,73 @@ let apiResults = {
 	scope: "",
 };
 
-let initialized = false;
-let lockdown = false;
+module.exports = class extends coreClass {
+	constructor(name, moduleManager) {
+		super(name, moduleManager);
 
-let lib = {
-	init: (cb) => {
-		async.waterfall([
-			(next) => {
-				cache.hget("api", "spotify", next, true);
-			},
+		this.dependsOn = ["cache"];
+	}
 
-			(data, next) => {
-				if (data) apiResults = data;
-				next();
-			}
-		], (err) => {
-			if (lockdown) return this._lockdown();
-			if (err) {
-				err = utils.getError(err);
-				cb(err);
-			} else {
-				initialized = true;
-				cb();
-			}
+	initialize() {
+		return new Promise((resolve, reject) => {
+			this.setStage(1);
+
+			this.cache = this.moduleManager.modules["cache"];
+			this.utils = this.moduleManager.modules["utils"];
+
+			const client = config.get("apis.spotify.client");
+			const secret = config.get("apis.spotify.secret");
+
+			const OAuth2 = require('oauth').OAuth2;
+			this.SpotifyOauth = new OAuth2(
+				client,
+				secret, 
+				'https://accounts.spotify.com/', 
+				null,
+				'api/token',
+				null);
+
+			async.waterfall([
+				(next) => {
+					this.setStage(2);
+					this.cache.hget("api", "spotify", next, true);
+				},
+	
+				(data, next) => {
+					this.setStage(3);
+					if (data) apiResults = data;
+					next();
+				}
+			], async (err) => {
+				if (err) {
+					err = await this.utils.getError(err);
+					reject(err);
+				} else {
+					resolve();
+				}
+			});
 		});
-	},
-	getToken: () => {
+	}
+
+	async getToken() {
+		try { await this._validateHook(); } catch { return; }
+
 		return new Promise((resolve, reject) => {
 			if (Date.now() > apiResults.expires_at) {
-				lib.requestToken(() => {
+				this.requestToken(() => {
 					resolve(apiResults.access_token);
 				});
 			} else resolve(apiResults.access_token);
 		});
-	},
-	requestToken: (cb) => {
+	}
+
+	async requestToken(cb) {
+		try { await this._validateHook(); } catch { return; }
+
 		async.waterfall([
 			(next) => {
-				logger.info("SPOTIFY_REQUEST_TOKEN", "Requesting new Spotify token.");
-				SpotifyOauth.getOAuthAccessToken(
+				this.logger.info("SPOTIFY_REQUEST_TOKEN", "Requesting new Spotify token.");
+				this.SpotifyOauth.getOAuthAccessToken(
 					'',
 					{ 'grant_type': 'client_credentials' },
 					next
@@ -70,15 +86,10 @@ let lib = {
 			(access_token, refresh_token, results, next) => {
 				apiResults = results;
 				apiResults.expires_at = Date.now() + (results.expires_in * 1000);
-				cache.hset("api", "spotify", apiResults, next, true);
+				this.cache.hset("api", "spotify", apiResults, next, true);
 			}
 		], () => {
 			cb();
 		});
-	},
-	_lockdown: () => {
-		lockdown = true;
 	}
-};
-
-module.exports = lib;
+}

+ 194 - 180
backend/logic/stations.js

@@ -1,118 +1,142 @@
 'use strict';
 
-const cache = require('./cache');
-const db = require('./db');
-const io = require('./io');
-const utils = require('./utils');
-const logger = require('./logger');
-const songs = require('./songs');
-const notifications = require('./notifications');
+const coreClass = require("../core");
+
 const async = require('async');
 
 let subscription = null;
 
-let initialized = false;
-let lockdown = false;
-
-//TEMP
-cache.sub('station.pause', (stationId) => {
-	if (lockdown) return;
-	notifications.remove(`stations.nextSong?id=${stationId}`);
-});
-
-cache.sub('station.resume', (stationId) => {
-	if (lockdown) return;
-	module.exports.initializeStation(stationId)
-});
-
-cache.sub('station.queueUpdate', (stationId) => {
-	if (lockdown) return;
-	module.exports.getStation(stationId, (err, station) => {
-		if (!station.currentSong && station.queue.length > 0) {
-			module.exports.initializeStation(stationId);
-		}
-	});
-});
-
-cache.sub('station.newOfficialPlaylist', (stationId) => {
-	if (lockdown) return;
-	cache.hget("officialPlaylists", stationId, (err, playlistObj) => {
-		if (!err && playlistObj) {
-			utils.emitToRoom(`station.${stationId}`, "event:newOfficialPlaylist", playlistObj.songs);
-		}
-	})
-});
+module.exports = class extends coreClass {
+	constructor(name, moduleManager) {
+		super(name, moduleManager);
 
-module.exports = {
+		this.dependsOn = ["cache", "db", "utils"];
+	}
 
-	init: function(cb) {
-		async.waterfall([
-			(next) => {
-				cache.hgetall('stations', next);
-			},
+	initialize() {
+		return new Promise(async (resolve, reject) => {
+			this.setStage(1);
+
+			this.cache = this.moduleManager.modules["cache"];
+			this.db = this.moduleManager.modules["db"];
+			this.utils = this.moduleManager.modules["utils"];
+			this.songs = this.moduleManager.modules["songs"];
+			this.notifications = this.moduleManager.modules["notifications"];
+
+			this.defaultSong = {
+				songId: '60ItHLz5WEA',
+				title: 'Faded - Alan Walker',
+				duration: 212,
+				skipDuration: 0,
+				likes: -1,
+				dislikes: -1
+			};
+
+			//TEMP
+			this.cache.sub('station.pause', async (stationId) => {
+				try { await this._validateHook(); } catch { return; }
+
+				this.notifications.remove(`stations.nextSong?id=${stationId}`);
+			});
 
-			(stations, next) => {
-				if (!stations) return next();
-				let stationIds = Object.keys(stations);
-				async.each(stationIds, (stationId, next) => {
-					db.models.station.findOne({_id: stationId}, (err, station) => {
-						if (err) next(err);
-						else if (!station) {
-							cache.hdel('stations', stationId, next);
-						} else next();
-					});
-				}, next);
-			},
+			this.cache.sub('station.resume', async (stationId) => {
+				try { await this._validateHook(); } catch { return; }
 
-			(next) => {
-				db.models.station.find({}, next);
-			},
+				this.initializeStation(stationId)
+			});
 
-			(stations, next) => {
-				async.each(stations, (station, next) => {
-					async.waterfall([
-						(next) => {
-							cache.hset('stations', station._id, cache.schemas.station(station), next);
-						},
+			this.cache.sub('station.queueUpdate', async (stationId) => {
+				try { await this._validateHook(); } catch { return; }
 
-						(station, next) => {
-							this.initializeStation(station._id, next);
-						}
-					], (err) => {
-						next(err);
-					});
-				}, next);
-			}
-		], (err) => {
-			if (lockdown) return this._lockdown();
-			if (err) {
-				err = utils.getError(err);
-				cb(err);
-			} else {
-				initialized = true;
-				cb();
-			}
+				this.getStation(stationId, (err, station) => {
+					if (!station.currentSong && station.queue.length > 0) {
+						this.initializeStation(stationId);
+					}
+				});
+			});
+
+			this.cache.sub('station.newOfficialPlaylist', async (stationId) => {
+				try { await this._validateHook(); } catch { return; }
+
+				this.cache.hget("officialPlaylists", stationId, (err, playlistObj) => {
+					if (!err && playlistObj) {
+						this.utils.emitToRoom(`station.${stationId}`, "event:newOfficialPlaylist", playlistObj.songs);
+					}
+				})
+			});
+
+
+			async.waterfall([
+				(next) => {
+					this.setStage(2);
+					this.cache.hgetall('stations', next);
+				},
+	
+				(stations, next) => {
+					this.setStage(3);
+					if (!stations) return next();
+					let stationIds = Object.keys(stations);
+					async.each(stationIds, (stationId, next) => {
+						this.db.models.station.findOne({_id: stationId}, (err, station) => {
+							if (err) next(err);
+							else if (!station) {
+								this.cache.hdel('stations', stationId, next);
+							} else next();
+						});
+					}, next);
+				},
+	
+				(next) => {
+					this.setStage(4);
+					this.db.models.station.find({}, next);
+				},
+	
+				(stations, next) => {
+					this.setStage(4);
+					async.each(stations, (station, next) => {
+						async.waterfall([
+							(next) => {
+								this.cache.hset('stations', station._id, this.cache.schemas.station(station), next);
+							},
+	
+							(station, next) => {
+								this.initializeStation(station._id, next);
+							}
+						], (err) => {
+							next(err);
+						});
+					}, next);
+				}
+			], async (err) => {
+				if (err) {
+					err = await this.utils.getError(err);
+					reject(err);
+				} else {
+					resolve();
+				}
+			});
 		});
-	},
+	}
+
+	async initializeStation(stationId, cb) {
+		try { await this._validateHook(); } catch { return; }
 
-	initializeStation: function(stationId, cb) {
-		if (lockdown) return;
 		if (typeof cb !== 'function') cb = ()=>{};
-		let _this = this;
+
 		async.waterfall([
 			(next) => {
-				_this.getStation(stationId, next);
+				this.getStation(stationId, next);
 			},
 			(station, next) => {
 				if (!station) return next('Station not found.');
-				notifications.unschedule(`stations.nextSong?id=${station._id}`);
-				subscription = notifications.subscribe(`stations.nextSong?id=${station._id}`, _this.skipStation(station._id), true, station);
+				this.notifications.unschedule(`stations.nextSong?id=${station._id}`);
+				subscription = this.notifications.subscribe(`stations.nextSong?id=${station._id}`, this.skipStation(station._id), true, station);
 				if (station.paused) return next(true, station);
 				next(null, station);
 			},
 			(station, next) => {
 				if (!station.currentSong) {
-					return _this.skipStation(station._id)((err, station) => {
+					return this.skipStation(station._id)((err, station) => {
 						if (err) return next(err);
 						return next(true, station);
 					});
@@ -124,7 +148,7 @@ module.exports = {
 						next(err, station);
 					});
 				} else {
-					notifications.schedule(`stations.nextSong?id=${station._id}`, timeLeft, null, station);
+					this.notifications.schedule(`stations.nextSong?id=${station._id}`, timeLeft, null, station);
 					next(null, station);
 				}
 			}
@@ -132,17 +156,17 @@ module.exports = {
 			if (err && err !== true) return cb(err);
 			cb(null, station);
 		});
-	},
+	}
+
+	async calculateSongForStation(station, cb) {
+		try { await this._validateHook(); } catch { return; }
 
-	calculateSongForStation: function(station, cb) {
-		if (lockdown) return;
-		let _this = this;
 		let songList = [];
 		async.waterfall([
 			(next) => {
 				let genresDone = [];
 				station.genres.forEach((genre) => {
-					db.models.song.find({genres: genre}, (err, songs) => {
+					this.db.models.song.find({genres: genre}, (err, songs) => {
 						if (!err) {
 							songs.forEach((song) => {
 								if (songList.indexOf(song._id) === -1) {
@@ -171,16 +195,20 @@ module.exports = {
 					if (songList.indexOf(songId) !== -1) playlist.push(songId);
 				});
 
-				playlist = utils.shuffle(playlist);
+				this.utils.shuffle(playlist).then((playlist) => {
+					next(null, playlist);
+				});
+			},
 
-				_this.calculateOfficialPlaylistList(station._id, playlist, () => {
+			(playlist, next) => {
+				this.calculateOfficialPlaylistList(station._id, playlist, () => {
 					next(null, playlist);
 				});
 			},
 
 			(playlist, next) => {
-				db.models.station.updateOne({_id: station._id}, {$set: {playlist: playlist}}, {runValidators: true}, (err) => {
-					_this.updateStation(station._id, () => {
+				this.db.models.station.updateOne({_id: station._id}, {$set: {playlist: playlist}}, {runValidators: true}, (err) => {
+					this.updateStation(station._id, () => {
 						next(err, playlist);
 					});
 				});
@@ -189,29 +217,29 @@ module.exports = {
 		], (err, newPlaylist) => {
 			cb(err, newPlaylist);
 		});
-	},
+	}
 
 	// Attempts to get the station from Redis. If it's not in Redis, get it from Mongo and add it to Redis.
-	getStation: function(stationId, cb) {
-		if (lockdown) return;
-		let _this = this;
+	async getStation(stationId, cb) {
+		try { await this._validateHook(); } catch { return; }
+
 		async.waterfall([
 			(next) => {
-				cache.hget('stations', stationId, next);
+				this.cache.hget('stations', stationId, next);
 			},
 
 			(station, next) => {
 				if (station) return next(true, station);
-				db.models.station.findOne({ _id: stationId }, next);
+				this.db.models.station.findOne({ _id: stationId }, next);
 			},
 
 			(station, next) => {
 				if (station) {
 					if (station.type === 'official') {
-						_this.calculateOfficialPlaylistList(station._id, station.playlist, () => {});
+						this.calculateOfficialPlaylistList(station._id, station.playlist, () => {});
 					}
-					station = cache.schemas.station(station);
-					cache.hset('stations', stationId, station);
+					station = this.cache.schemas.station(station);
+					this.cache.hset('stations', stationId, station);
 					next(true, station);
 				} else next('Station not found');
 			},
@@ -220,25 +248,25 @@ module.exports = {
 			if (err && err !== true) return cb(err);
 			cb(null, station);
 		});
-	},
+	}
 
 	// Attempts to get the station from Redis. If it's not in Redis, get it from Mongo and add it to Redis.
-	getStationByName: function(stationName, cb) {
-		if (lockdown) return;
-		let _this = this;
+	async getStationByName(stationName, cb) {
+		try { await this._validateHook(); } catch { return; }
+
 		async.waterfall([
 
 			(next) => {
-				db.models.station.findOne({ name: stationName }, next);
+				this.db.models.station.findOne({ name: stationName }, next);
 			},
 
 			(station, next) => {
 				if (station) {
 					if (station.type === 'official') {
-						_this.calculateOfficialPlaylistList(station._id, station.playlist, ()=>{});
+						this.calculateOfficialPlaylistList(station._id, station.playlist, ()=>{});
 					}
-					station = cache.schemas.station(station);
-					cache.hset('stations', station._id, station);
+					station = this.cache.schemas.station(station);
+					this.cache.hset('stations', station._id, station);
 					next(true, station);
 				} else next('Station not found');
 			},
@@ -247,36 +275,37 @@ module.exports = {
 			if (err && err !== true) return cb(err);
 			cb(null, station);
 		});
-	},
+	}
+
+	async updateStation(stationId, cb) {
+		try { await this._validateHook(); } catch { return; }
 
-	updateStation: function(stationId, cb) {
-		if (lockdown) return;
-		let _this = this;
 		async.waterfall([
 
 			(next) => {
-				db.models.station.findOne({ _id: stationId }, next);
+				this.db.models.station.findOne({ _id: stationId }, next);
 			},
 
 			(station, next) => {
 				if (!station) {
-					cache.hdel('stations', stationId);
+					this.cache.hdel('stations', stationId);
 					return next('Station not found');
 				}
-				cache.hset('stations', stationId, station, next);
+				this.cache.hset('stations', stationId, station, next);
 			}
 
 		], (err, station) => {
 			if (err && err !== true) return cb(err);
 			cb(null, station);
 		});
-	},
+	}
+
+	async calculateOfficialPlaylistList(stationId, songList, cb) {
+		try { await this._validateHook(); } catch { return; }
 
-	calculateOfficialPlaylistList: (stationId, songList, cb) => {
-		if (lockdown) return;
 		let lessInfoPlaylist = [];
 		async.each(songList, (song, next) => {
-			songs.getSong(song, (err, song) => {
+			this.songs.getSong(song, (err, song) => {
 				if (!err && song) {
 					let newSong = {
 						songId: song.songId,
@@ -289,36 +318,35 @@ module.exports = {
 				next();
 			});
 		}, () => {
-			cache.hset("officialPlaylists", stationId, cache.schemas.officialPlaylist(stationId, lessInfoPlaylist), () => {
-				cache.pub("station.newOfficialPlaylist", stationId);
+			this.cache.hset("officialPlaylists", stationId, this.cache.schemas.officialPlaylist(stationId, lessInfoPlaylist), () => {
+				this.cache.pub("station.newOfficialPlaylist", stationId);
 				cb();
 			});
 		});
-	},
-
-	skipStation: function(stationId) {
-		if (lockdown) return;
-		logger.info("STATION_SKIP", `Skipping station ${stationId}.`, false);
-		let _this = this;
-		return (cb) => {
-			if (lockdown) return;
+	}
+
+	skipStation(stationId) {
+		this.logger.info("STATION_SKIP", `Skipping station ${stationId}.`, false);
+		return async (cb) => {
+			try { await this._validateHook(); } catch { return; }
+
 			if (typeof cb !== 'function') cb = ()=>{};
 
 			async.waterfall([
 				(next) => {
-					_this.getStation(stationId, next);
+					this.getStation(stationId, next);
 				},
 				(station, next) => {
 					if (!station) return next('Station not found.');
 					if (station.type === 'community' && station.partyMode && station.queue.length === 0) return next(null, null, -11, station); // Community station with party mode enabled and no songs in the queue
 					if (station.type === 'community' && station.partyMode && station.queue.length > 0) { // Community station with party mode enabled and songs in the queue
-						return db.models.station.updateOne({_id: stationId}, {$pull: {queue: {_id: station.queue[0]._id}}}, (err) => {
+						return this.db.models.station.updateOne({_id: stationId}, {$pull: {queue: {_id: station.queue[0]._id}}}, (err) => {
 							if (err) return next(err);
 							next(null, station.queue[0], -12, station);
 						});
 					}
 					if (station.type === 'community' && !station.partyMode) {
-						return db.models.playlist.findOne({_id: station.privatePlaylist}, (err, playlist) => {
+						return this.db.models.playlist.findOne({_id: station.privatePlaylist}, (err, playlist) => {
 							if (err) return next(err);
 							if (!playlist) return next(null, null, -13, station);
 							playlist = playlist.songs;
@@ -341,18 +369,18 @@ module.exports = {
 										return next(null, currentSong, currentSongIndex, station);
 									}
 								};
-								if (playlist[currentSongIndex]._id) songs.getSong(playlist[currentSongIndex]._id, callback);
-								else songs.getSongFromId(playlist[currentSongIndex].songId, callback);
+								if (playlist[currentSongIndex]._id) this.songs.getSong(playlist[currentSongIndex]._id, callback);
+								else this.songs.getSongFromId(playlist[currentSongIndex].songId, callback);
 							} else return next(null, null, -14, station);
 						});
 					}
 					if (station.type === 'official' && station.playlist.length === 0) {
-						return _this.calculateSongForStation(station, (err, playlist) => {
+						return this.calculateSongForStation(station, (err, playlist) => {
 							if (err) return next(err);
-							if (playlist.length === 0) return next(null, _this.defaultSong, 0, station);
+							if (playlist.length === 0) return next(null, this.defaultSong, 0, station);
 							else {
-								songs.getSong(playlist[0], (err, song) => {
-									if (err || !song) return next(null, _this.defaultSong, 0, station);
+								this.songs.getSong(playlist[0], (err, song) => {
+									if (err || !song) return next(null, this.defaultSong, 0, station);
 									return next(null, song, 0, station);
 								});
 							}
@@ -361,7 +389,7 @@ module.exports = {
 					if (station.type === 'official' && station.playlist.length > 0) {
 						async.doUntil((next) => {
 							if (station.currentSongIndex < station.playlist.length - 1) {
-								songs.getSong(station.playlist[station.currentSongIndex + 1], (err, song) => {
+								this.songs.getSong(station.playlist[station.currentSongIndex + 1], (err, song) => {
 									if (!err) return next(null, song, station.currentSongIndex + 1);
 									else {
 										station.currentSongIndex++;
@@ -369,10 +397,10 @@ module.exports = {
 									}
 								});
 							} else {
-								_this.calculateSongForStation(station, (err, newPlaylist) => {
-									if (err) return next(null, _this.defaultSong, 0);
-									songs.getSong(newPlaylist[0], (err, song) => {
-										if (err || !song) return next(null, _this.defaultSong, 0);
+								this.calculateSongForStation(station, (err, newPlaylist) => {
+									if (err) return next(null, this.defaultSong, 0);
+									this.songs.getSong(newPlaylist[0], (err, song) => {
+										if (err || !song) return next(null, this.defaultSong, 0);
 										station.playlist = newPlaylist;
 										next(null, song, 0);
 									});
@@ -418,37 +446,37 @@ module.exports = {
 				},
 
 				($set, station, next) => {
-					db.models.station.updateOne({_id: station._id}, {$set}, (err) => {
-						_this.updateStation(station._id, (err, station) => {
+					this.db.models.station.updateOne({_id: station._id}, {$set}, (err) => {
+						this.updateStation(station._id, (err, station) => {
 							if (station.type === 'community' && station.partyMode === true)
-								cache.pub('station.queueUpdate', stationId);
+								this.cache.pub('station.queueUpdate', stationId);
 							next(null, station);
 						});
 					});
 				},
-			], (err, station) => {
+			], async (err, station) => {
 				if (!err) {
 					if (station.currentSong !== null && station.currentSong.songId !== undefined) {
 						station.currentSong.skipVotes = 0;
 					}
 					//TODO Pub/Sub this
-					utils.emitToRoom(`station.${station._id}`, "event:songs.next", {
+					this.utils.emitToRoom(`station.${station._id}`, "event:songs.next", {
 						currentSong: station.currentSong,
 						startedAt: station.startedAt,
 						paused: station.paused,
 						timePaused: 0
 					});
 
-					if (station.privacy === 'public') utils.emitToRoom('home', "event:station.nextSong", station._id, station.currentSong);
+					if (station.privacy === 'public') this.utils.emitToRoom('home', "event:station.nextSong", station._id, station.currentSong);
 					else {
-						let sockets = utils.getRoomSockets('home');
+						let sockets = await this.utils.getRoomSockets('home');
 						for (let socketId in sockets) {
 							let socket = sockets[socketId];
 							let session = sockets[socketId].session;
 							if (session.sessionId) {
-								cache.hget('sessions', session.sessionId, (err, session) => {
+								this.cache.hget('sessions', session.sessionId, (err, session) => {
 									if (!err && session) {
-										db.models.user.findOne({_id: session.userId}, (err, user) => {
+										this.db.models.user.findOne({_id: session.userId}, (err, user) => {
 											if (!err && user) {
 												if (user.role === 'admin') socket.emit("event:station.nextSong", station._id, station.currentSong);
 												else if (station.type === "community" && station.owner === session.userId) socket.emit("event:station.nextSong", station._id, station.currentSong);
@@ -460,34 +488,20 @@ module.exports = {
 						}
 					}
 					if (station.currentSong !== null && station.currentSong.songId !== undefined) {
-						utils.socketsJoinSongRoom(utils.getRoomSockets(`station.${station._id}`), `song.${station.currentSong.songId}`);
+						this.utils.socketsJoinSongRoom(await this.utils.getRoomSockets(`station.${station._id}`), `song.${station.currentSong.songId}`);
 						if (!station.paused) {
-							notifications.schedule(`stations.nextSong?id=${station._id}`, station.currentSong.duration * 1000, null, station);
+							this.notifications.schedule(`stations.nextSong?id=${station._id}`, station.currentSong.duration * 1000, null, station);
 						}
 					} else {
-						utils.socketsLeaveSongRooms(utils.getRoomSockets(`station.${station._id}`));
+						this.utils.socketsLeaveSongRooms(await this.utils.getRoomSockets(`station.${station._id}`));
 					}
 					cb(null, station);
 				} else {
-					err = utils.getError(err);
+					err = await this.utils.getError(err);
 					logger.error('SKIP_STATION', `Skipping station "${stationId}" failed. "${err}"`);
 					cb(err);
 				}
 			});
 		}
-	},
-
-	defaultSong: {
-		songId: '60ItHLz5WEA',
-		title: 'Faded - Alan Walker',
-		duration: 212,
-		skipDuration: 0,
-		likes: -1,
-		dislikes: -1
-	},
-
-	_lockdown: () => {
-		lockdown = true;
 	}
-
-};
+}

+ 127 - 120
backend/logic/tasks.js

@@ -1,113 +1,32 @@
 'use strict';
 
-const cache = require("./cache");
-const logger = require("./logger");
-const Stations = require("./stations");
-const notifications = require("./notifications");
+const coreClass = require("../core");
+
 const async = require("async");
-let utils;
+
 let tasks = {};
 
-let testTask = (callback) => {
-	//Stuff
-	console.log("Starting task");
-	setTimeout(() => {
-		console.log("Callback");
-		callback();
-	}, 10000);
-};
-
-let checkStationSkipTask = (callback) => {
-	logger.info("TASK_STATIONS_SKIP_CHECK", `Checking for stations to be skipped.`, false);
-	async.waterfall([
-		(next) => {
-			cache.hgetall('stations', next);
-		},
-		(stations, next) => {
-			async.each(stations, (station, next2) => {
-				if (station.paused || !station.currentSong || !station.currentSong.title) return next2();
-				const timeElapsed = Date.now() - station.startedAt - station.timePaused;
-				if (timeElapsed <= station.currentSong.duration) return next2();
-				else {
-					logger.error("TASK_STATIONS_SKIP_CHECK", `Skipping ${station._id} as it should have skipped already.`);
-					Stations.initializeStation(station._id);
-					next2();
-				}
-			}, () => {
-				next();
-			});
-		}
-	], () => {
-		callback();
-	});
-};
-
-let sessionClearingTask = (callback) => {
-	logger.info("TASK_SESSION_CLEAR", `Checking for sessions to be cleared.`, false);
-	async.waterfall([
-		(next) => {
-			cache.hgetall('sessions', next);
-		},
-		(sessions, next) => {
-			if (!sessions) return next();
-			let keys = Object.keys(sessions);
-			async.each(keys, (sessionId, next2) => {
-				let session = sessions[sessionId];
-				if (session && session.refreshDate && (Date.now() - session.refreshDate) < (60 * 60 * 24 * 30 * 1000)) return next2();
-				if (!session) {
-					logger.info("TASK_SESSION_CLEAR", 'Removing an empty session.');
-					cache.hdel('sessions', sessionId, () => {
-						next2();
-					});
-				} else if (!session.refreshDate) {
-					session.refreshDate = Date.now();
-					cache.hset('sessions', sessionId, session, () => {
-						next2();
-					});
-				} else if ((Date.now() - session.refreshDate) > (60 * 60 * 24 * 30 * 1000)) {
-					utils.socketsFromSessionId(session.sessionId, (sockets) => {
-						if (sockets.length > 0) {
-							session.refreshDate = Date.now();
-							cache.hset('sessions', sessionId, session, () => {
-								next2()
-							});
-						} else {
-							logger.info("TASK_SESSION_CLEAR", `Removing session ${sessionId} for user ${session.userId} since inactive for 30 days and not currently in use.`);
-							cache.hdel('sessions', session.sessionId, () => {
-								next2();
-							});
-						}
-					});
-				} else {
-					logger.error("TASK_SESSION_CLEAR", "This should never log.");
-					next2();
-				}
-			}, () => {
-				next();
-			});
-		}
-	], () => {
-		callback();
-	});
-};
-
-let initialized = false;
-let lockdown = false;
-
-module.exports = {
-	init: function(cb) {
-		utils = require('./utils');
-		this.createTask("testTask", testTask, 5000, true);
-		this.createTask("stationSkipTask", checkStationSkipTask, 1000 * 60 * 30);
-		this.createTask("sessionClearTask", sessionClearingTask, 1000 * 60 * 60 * 6);
-
-		initialized = true;
-
-		if (lockdown) return this._lockdown();
-		cb();
-	},
-	createTask: function(name, fn, timeout, paused = false) {
-		if (lockdown) return;
+module.exports = class extends coreClass {
+	initialize() {
+		return new Promise((resolve, reject) => {
+			this.setStage(1);
+			
+			this.cache = this.moduleManager.modules["cache"];
+			this.stations = this.moduleManager.modules["stations"];
+			this.notifications = this.moduleManager.modules["notifications"];
+			this.utils = this.moduleManager.modules["utils"];
+
+			//this.createTask("testTask", testTask, 5000, true);
+			this.createTask("stationSkipTask", this.checkStationSkipTask, 1000 * 60 * 30);
+			this.createTask("sessionClearTask", this.sessionClearingTask, 1000 * 60 * 60 * 6);
+
+			resolve();
+		});
+	}
+
+	async createTask(name, fn, timeout, paused = false) {
+		try { await this._validateHook(); } catch { return; }
+
 		tasks[name] = {
 			name,
 			fn,
@@ -116,15 +35,23 @@ module.exports = {
 			timer: null
 		};
 		if (!paused) this.handleTask(tasks[name]);
-	},
-	pauseTask: (name) => {
+	}
+
+	async pauseTask(name) {
+		try { await this._validateHook(); } catch { return; }
+
 		if (tasks[name].timer) tasks[name].timer.pause();
-	},
-	resumeTask: (name) => {
+	}
+
+	async resumeTask(name) {
+		try { await this._validateHook(); } catch { return; }
+
 		tasks[name].timer.resume();
-	},
-	handleTask: function(task) {
-		if (lockdown) return;
+	}
+
+	async handleTask(task) {
+		try { await this._validateHook(); } catch { return; }
+
 		if (task.timer) task.timer.pause();
 
 		task.fn(() => {
@@ -133,12 +60,92 @@ module.exports = {
 				this.handleTask(task);
 			}, task.timeout, false);
 		});
-	},
-	_lockdown: function() {
-		for (let key in tasks) {
-			this.pauseTask(key);
-		}
-		tasks = {};
-		lockdown = true;
 	}
-};
+
+	/*testTask(callback) {
+		//Stuff
+		console.log("Starting task");
+		setTimeout(() => {
+			console.log("Callback");
+			callback();
+		}, 10000);
+	}*/
+
+	async checkStationSkipTask(callback) {
+		try { await this._validateHook(); } catch { return; }
+
+		this.logger.info("TASK_STATIONS_SKIP_CHECK", `Checking for stations to be skipped.`, false);
+		async.waterfall([
+			(next) => {
+				this.cache.hgetall('stations', next);
+			},
+			(stations, next) => {
+				async.each(stations, (station, next2) => {
+					if (station.paused || !station.currentSong || !station.currentSong.title) return next2();
+					const timeElapsed = Date.now() - station.startedAt - station.timePaused;
+					if (timeElapsed <= station.currentSong.duration) return next2();
+					else {
+						this.logger.error("TASK_STATIONS_SKIP_CHECK", `Skipping ${station._id} as it should have skipped already.`);
+						this.stations.initializeStation(station._id);
+						next2();
+					}
+				}, () => {
+					next();
+				});
+			}
+		], () => {
+			callback();
+		});
+	}
+
+	async sessionClearingTask(callback) {
+		try { await this._validateHook(); } catch { return; }
+	
+		this.logger.info("TASK_SESSION_CLEAR", `Checking for sessions to be cleared.`, false);
+		async.waterfall([
+			(next) => {
+				this.cache.hgetall('sessions', next);
+			},
+			(sessions, next) => {
+				if (!sessions) return next();
+				let keys = Object.keys(sessions);
+				async.each(keys, (sessionId, next2) => {
+					let session = sessions[sessionId];
+					if (session && session.refreshDate && (Date.now() - session.refreshDate) < (60 * 60 * 24 * 30 * 1000)) return next2();
+					if (!session) {
+						this.logger.info("TASK_SESSION_CLEAR", 'Removing an empty session.');
+						this.cache.hdel('sessions', sessionId, () => {
+							next2();
+						});
+					} else if (!session.refreshDate) {
+						session.refreshDate = Date.now();
+						this.cache.hset('sessions', sessionId, session, () => {
+							next2();
+						});
+					} else if ((Date.now() - session.refreshDate) > (60 * 60 * 24 * 30 * 1000)) {
+						this.utils.socketsFromSessionId(session.sessionId, (sockets) => {
+							if (sockets.length > 0) {
+								session.refreshDate = Date.now();
+								this.cache.hset('sessions', sessionId, session, () => {
+									next2()
+								});
+							} else {
+								this.logger.info("TASK_SESSION_CLEAR", `Removing session ${sessionId} for user ${session.userId} since inactive for 30 days and not currently in use.`);
+								this.cache.hdel('sessions', session.sessionId, () => {
+									next2();
+								});
+							}
+						});
+					} else {
+						this.logger.error("TASK_SESSION_CLEAR", "This should never log.");
+						next2();
+					}
+				}, () => {
+					next();
+				});
+			}
+		], () => {
+			callback();
+		});
+	}
+}

+ 208 - 127
backend/logic/utils.js

@@ -1,13 +1,10 @@
 'use strict';
 
-const moment  = require('moment'),
-	  io      = require('./io'),
-	  db      = require('./db'),
-	  spotify = require('./spotify'),
-	  config  = require('config'),
+const coreClass = require("../core");
+
+const config  = require('config'),
 	  async	  = require('async'),
-	  request = require('request'),
-	  cache   = require('./cache');
+	  request = require('request');
 
 class Timer {
 	constructor(callback, delay, paused) {
@@ -56,96 +53,135 @@ class Timer {
 			return Date.now() - this.timePaused;
 		}
 	}
-}
-
-function convertTime (duration) {
-	let a = duration.match(/\d+/g);
+} 
 
-	if (duration.indexOf('M') >= 0 && duration.indexOf('H') == -1 && duration.indexOf('S') == -1) {
-		a = [0, a[0], 0];
-	}
+let youtubeRequestCallbacks = [];
+let youtubeRequestsPending = 0;
+let youtubeRequestsActive = false;
 
-	if (duration.indexOf('H') >= 0 && duration.indexOf('M') == -1) {
-		a = [a[0], 0, a[1]];
-	}
-	if (duration.indexOf('H') >= 0 && duration.indexOf('M') == -1 && duration.indexOf('S') == -1) {
-		a = [a[0], 0, 0];
-	}
+module.exports = class extends coreClass {
+	initialize() {
+		return new Promise((resolve, reject) => {
+			this.setStage(1);
+			
+			this.io = this.moduleManager.modules["io"];
+			this.db = this.moduleManager.modules["db"];
+			this.spotify = this.moduleManager.modules["spotify"];
+			this.cache = this.moduleManager.modules["cache"];
 
-	duration = 0;
+			this.Timer = Timer;
 
-	if (a.length == 3) {
-		duration = duration + parseInt(a[0]) * 3600;
-		duration = duration + parseInt(a[1]) * 60;
-		duration = duration + parseInt(a[2]);
+			resolve();
+		});
 	}
 
-	if (a.length == 2) {
-		duration = duration + parseInt(a[0]) * 60;
-		duration = duration + parseInt(a[1]);
+	async parseCookies(cookieString) {
+		try { await this._validateHook(); } catch { return; }
+		let cookies = {};
+		if (cookieString) cookieString.split("; ").map((cookie) => {
+			(cookies[cookie.substring(0, cookie.indexOf("="))] = cookie.substring(cookie.indexOf("=") + 1, cookie.length));
+		});
+		return cookies;
 	}
 
-	if (a.length == 1) {
-		duration = duration + parseInt(a[0]);
+	async cookiesToString(cookies) {
+		try { await this._validateHook(); } catch { return; }
+		let newCookie = [];
+		for (let prop in cookie) {
+			newCookie.push(prop + "=" + cookie[prop]);
+		}
+		return newCookie.join("; ");
 	}
 
-	let hours = Math.floor(duration / 3600);
-	let minutes = Math.floor(duration % 3600 / 60);
-	let seconds = Math.floor(duration % 3600 % 60);
-
-	return (hours < 10 ? ("0" + hours + ":") : (hours + ":")) + (minutes < 10 ? ("0" + minutes + ":") : (minutes + ":")) + (seconds < 10 ? ("0" + seconds) : seconds);
-}
+	async removeCookie(cookieString, cookieName) {
+		try { await this._validateHook(); } catch { return; }
+		var cookies = this.parseCookies(cookieString);
+		delete cookies[cookieName];
+		return this.toString(cookies);
+	}
 
-let youtubeRequestCallbacks = [];
-let youtubeRequestsPending = 0;
-let youtubeRequestsActive = false;
+	async htmlEntities(str) {
+		try { await this._validateHook(); } catch { return; }
+		return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
+	}
 
-module.exports = {
-	htmlEntities: str => String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'),
-	generateRandomString: function(len) {
+	async generateRandomString(len) {
+		try { await this._validateHook(); } catch { return; }
 		let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".split("");
 		let result = [];
 		for (let i = 0; i < len; i++) {
-			result.push(chars[this.getRandomNumber(0, chars.length - 1)]);
+			result.push(chars[await this.getRandomNumber(0, chars.length - 1)]);
 		}
 		return result.join("");
-	},
-	getSocketFromId: function(socketId) {
+	}
+
+	async getSocketFromId(socketId) {
+		try { await this._validateHook(); } catch { return; }
 		return globals.io.sockets.sockets[socketId];
-	},
-	getRandomNumber: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min,
-	convertTime,
-	Timer,
-	guid: () => [1,1,0,1,0,1,0,1,0,1,1,1].map(b => b ? Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1) : '-').join(''),
-	cookies: {
-		parseCookies: cookieString => {
-			let cookies = {};
-			if (cookieString) cookieString.split("; ").map((cookie) => {
-				(cookies[cookie.substring(0, cookie.indexOf("="))] = cookie.substring(cookie.indexOf("=") + 1, cookie.length));
-			});
-			return cookies;
-		},
-		toString: cookies => {
-			let newCookie = [];
-			for (let prop in cookie) {
-				newCookie.push(prop + "=" + cookie[prop]);
-			}
-			return newCookie.join("; ");
-		},
-		removeCookie: (cookieString, cookieName) => {
-			var cookies = this.parseCookies(cookieString);
-			delete cookies[cookieName];
-			return this.toString(cookies);
+	}
+
+	async getRandomNumber(min, max) {
+		try { await this._validateHook(); } catch { return; }
+		return Math.floor(Math.random() * (max - min + 1)) + min
+	}
+
+	async convertTime(duration) {
+		try { await this._validateHook(); } catch { return; }
+		let a = duration.match(/\d+/g);
+	
+		if (duration.indexOf('M') >= 0 && duration.indexOf('H') == -1 && duration.indexOf('S') == -1) {
+			a = [0, a[0], 0];
+		}
+	
+		if (duration.indexOf('H') >= 0 && duration.indexOf('M') == -1) {
+			a = [a[0], 0, a[1]];
+		}
+		if (duration.indexOf('H') >= 0 && duration.indexOf('M') == -1 && duration.indexOf('S') == -1) {
+			a = [a[0], 0, 0];
 		}
-	},
-	socketFromSession: function(socketId) {
-		let ns = io.io.of("/");
+	
+		duration = 0;
+	
+		if (a.length == 3) {
+			duration = duration + parseInt(a[0]) * 3600;
+			duration = duration + parseInt(a[1]) * 60;
+			duration = duration + parseInt(a[2]);
+		}
+	
+		if (a.length == 2) {
+			duration = duration + parseInt(a[0]) * 60;
+			duration = duration + parseInt(a[1]);
+		}
+	
+		if (a.length == 1) {
+			duration = duration + parseInt(a[0]);
+		}
+	
+		let hours = Math.floor(duration / 3600);
+		let minutes = Math.floor(duration % 3600 / 60);
+		let seconds = Math.floor(duration % 3600 % 60);
+	
+		return (hours < 10 ? ("0" + hours + ":") : (hours + ":")) + (minutes < 10 ? ("0" + minutes + ":") : (minutes + ":")) + (seconds < 10 ? ("0" + seconds) : seconds);
+	}
+
+	async guid () {
+		try { await this._validateHook(); } catch { return; }
+		return [1,1,0,1,0,1,0,1,0,1,1,1].map(b => b ? Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1) : '-').join('');
+	}
+
+	async socketFromSession(socketId) {
+		try { await this._validateHook(); } catch { return; }
+
+		let ns = this.io.io.of("/");
 		if (ns) {
 			return ns.connected[socketId];
 		}
-	},
-	socketsFromSessionId: function(sessionId, cb) {
-		let ns = io.io.of("/");
+	}
+
+	async socketsFromSessionId(sessionId, cb) {
+		try { await this._validateHook(); } catch { return; }
+
+		let ns = this.io.io.of("/");
 		let sockets = [];
 		if (ns) {
 			async.each(Object.keys(ns.connected), (id, next) => {
@@ -156,14 +192,17 @@ module.exports = {
 				cb(sockets);
 			});
 		}
-	},
-	socketsFromUser: function(userId, cb) {
-		let ns = io.io.of("/");
+	}
+
+	async socketsFromUser(userId, cb) {
+		try { await this._validateHook(); } catch { return; }
+
+		let ns = this.io.io.of("/");
 		let sockets = [];
 		if (ns) {
 			async.each(Object.keys(ns.connected), (id, next) => {
 				let session = ns.connected[id].session;
-				cache.hget('sessions', session.sessionId, (err, session) => {
+				this.cache.hget('sessions', session.sessionId, (err, session) => {
 					if (!err && session && session.userId === userId) sockets.push(ns.connected[id]);
 					next();
 				});
@@ -171,14 +210,17 @@ module.exports = {
 				cb(sockets);
 			});
 		}
-	},
-	socketsFromIP: function(ip, cb) {
-		let ns = io.io.of("/");
+	}
+
+	async socketsFromIP(ip, cb) {
+		try { await this._validateHook(); } catch { return; }
+
+		let ns = this.io.io.of("/");
 		let sockets = [];
 		if (ns) {
 			async.each(Object.keys(ns.connected), (id, next) => {
 				let session = ns.connected[id].session;
-				cache.hget('sessions', session.sessionId, (err, session) => {
+				this.cache.hget('sessions', session.sessionId, (err, session) => {
 					if (!err && session && ns.connected[id].ip === ip) sockets.push(ns.connected[id]);
 					next();
 				});
@@ -186,9 +228,12 @@ module.exports = {
 				cb(sockets);
 			});
 		}
-	},
-	socketsFromUserWithoutCache: function(userId, cb) {
-		let ns = io.io.of("/");
+	}
+
+	async socketsFromUserWithoutCache(userId, cb) {
+		try { await this._validateHook(); } catch { return; }
+
+		let ns = this.io.io.of("/");
 		let sockets = [];
 		if (ns) {
 			async.each(Object.keys(ns.connected), (id, next) => {
@@ -199,31 +244,43 @@ module.exports = {
 				cb(sockets);
 			});
 		}
-	},
-	socketLeaveRooms: function(socketid) {
-		let socket = this.socketFromSession(socketid);
+	}
+
+	async socketLeaveRooms(socketid) {
+		try { await this._validateHook(); } catch { return; }
+
+		let socket = await this.socketFromSession(socketid);
 		let rooms = socket.rooms;
 		for (let room in rooms) {
 			socket.leave(room);
 		}
-	},
-	socketJoinRoom: function(socketId, room) {
-		let socket = this.socketFromSession(socketId);
+	}
+
+	async socketJoinRoom(socketId, room) {
+		try { await this._validateHook(); } catch { return; }
+
+		let socket = await this.socketFromSession(socketId);
 		let rooms = socket.rooms;
 		for (let room in rooms) {
 			socket.leave(room);
 		}
 		socket.join(room);
-	},
-	socketJoinSongRoom: function(socketId, room) {
-		let socket = this.socketFromSession(socketId);
+	}
+
+	async socketJoinSongRoom(socketId, room) {
+		try { await this._validateHook(); } catch { return; }
+
+		let socket = await this.socketFromSession(socketId);
 		let rooms = socket.rooms;
 		for (let room in rooms) {
 			if (room.indexOf('song.') !== -1) socket.leave(rooms);
 		}
 		socket.join(room);
-	},
-	socketsJoinSongRoom: function(sockets, room) {
+	}
+
+	async socketsJoinSongRoom(sockets, room) {
+		try { await this._validateHook(); } catch { return; }
+
 		for (let id in sockets) {
 			let socket = sockets[id];
 			let rooms = socket.rooms;
@@ -232,8 +289,11 @@ module.exports = {
 			}
 			socket.join(room);
 		}
-	},
-	socketsLeaveSongRooms: function(sockets) {
+	}
+
+	async socketsLeaveSongRooms(sockets) {
+		try { await this._validateHook(); } catch { return; }
+
 		for (let id in sockets) {
 			let socket = sockets[id];
 			let rooms = socket.rooms;
@@ -241,30 +301,34 @@ module.exports = {
 				if (room.indexOf('song.') !== -1) socket.leave(room);
 			}
 		}
-	},
-	emitToRoom: function(room) {
-		let sockets = io.io.sockets.sockets;
+	}
+
+	async emitToRoom(room, ...args) {
+		try { await this._validateHook(); } catch { return; }
+
+		let sockets = this.io.io.sockets.sockets;
 		for (let id in sockets) {
 			let socket = sockets[id];
 			if (socket.rooms[room]) {
-				let args = [];
-				for (let i = 1; i < Object.keys(arguments).length; i++) {
-					args.push(arguments[i]);
-				}
 				socket.emit.apply(socket, args);
 			}
 		}
-	},
-	getRoomSockets: function(room) {
-		let sockets = io.io.sockets.sockets;
+	}
+
+	async getRoomSockets(room) {
+		try { await this._validateHook(); } catch { return; }
+
+		let sockets = this.io.io.sockets.sockets;
 		let roomSockets = [];
 		for (let id in sockets) {
 			let socket = sockets[id];
 			if (socket.rooms[room]) roomSockets.push(socket);
 		}
 		return roomSockets;
-	},
-	getSongFromYouTube: (songId, cb) => {
+	}
+
+	async getSongFromYouTube(songId, cb) {
+		try { await this._validateHook(); } catch { return; }
 
 		youtubeRequestCallbacks.push({cb: (test) => {
 			youtubeRequestsActive = true;
@@ -320,8 +384,10 @@ module.exports = {
 		if (!youtubeRequestsActive) {
 			youtubeRequestCallbacks[0].cb(youtubeRequestCallbacks[0].songId);
 		}
-	},
-	getPlaylistFromYouTube: (url, cb) => {
+	}
+
+	async getPlaylistFromYouTube(url, cb) {
+		try { await this._validateHook(); } catch { return; }
 
 		let name = 'list'.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
 		var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
@@ -353,8 +419,11 @@ module.exports = {
 			});
 		}
 		getPage(null, []);
-	},
-	getSongFromSpotify: async (song, cb) => {
+	}
+
+	async getSongFromSpotify(song, cb) {
+		try { await this._validateHook(); } catch { return; }
+
 		if (!config.get("apis.spotify.enabled")) return cb("Spotify is not enabled", null);
 
 		const spotifyParams = [
@@ -362,7 +431,7 @@ module.exports = {
 			`type=track`
 		].join('&');
 
-		const token = await spotify.getToken();
+		const token = await this.spotify.getToken();
 		const options = {
 			url: `https://api.spotify.com/v1/search?${spotifyParams}`,
 			headers: {
@@ -400,8 +469,11 @@ module.exports = {
 
 			cb(null, song);
 		});
-	},
-	getSongsFromSpotify: async (title, artist, cb) => {
+	}
+
+	async getSongsFromSpotify(title, artist, cb) {
+		try { await this._validateHook(); } catch { return; }
+
 		if (!config.get("apis.spotify.enabled")) return cb([]);
 
 		const spotifyParams = [
@@ -409,7 +481,7 @@ module.exports = {
 			`type=track`
 		].join('&');
 		
-		const token = await spotify.getToken();
+		const token = await this.spotify.getToken();
 		const options = {
 			url: `https://api.spotify.com/v1/search?${spotifyParams}`,
 			headers: {
@@ -449,8 +521,11 @@ module.exports = {
 
 			cb(songs);
 		});
-	},
-	shuffle: (array) => {
+	}
+
+	async shuffle(array) {
+		try { await this._validateHook(); } catch { return; }
+
 		let currentIndex = array.length, temporaryValue, randomIndex;
 
 		// While there remain elements to shuffle...
@@ -467,8 +542,11 @@ module.exports = {
 		}
 
 		return array;
-	},
-	getError: (err) => {
+	}
+
+	async getError(err) {
+		try { await this._validateHook(); } catch { return; }
+
 		let error = 'An error occurred.';
 		if (typeof err === "string") error = err;
 		else if (err.message) {
@@ -476,8 +554,11 @@ module.exports = {
 			else error = err.errors[Object.keys(err.errors)].message;
 		}
 		return error;
-	},
-	canUserBeInStation: (station, userId, cb) => {
+	}
+
+	async canUserBeInStation(station, userId, cb) {
+		try { await this._validateHook(); } catch { return; }
+
 		async.waterfall([
 			(next) => {
 				if (station.privacy !== 'private') return next(true);
@@ -486,7 +567,7 @@ module.exports = {
 			},
 
 			(next) => {
-				db.models.user.findOne({_id: userId}, next);
+				this.db.models.user.findOne({_id: userId}, next);
 			},
 
 			(user, next) => {
@@ -501,4 +582,4 @@ module.exports = {
 			return cb(false);
 		});
 	}
-};
+}

+ 1 - 1
backend/package.json

@@ -3,7 +3,7 @@
   "private": true,
   "version": "2.1.0",
   "description": "A modern, open-source, collaborative music app https://musare.com",
-  "main": "logic/app.js",
+  "main": "index.js",
   "author": "Musare Team",
   "license": "GPL-3.0",
   "repository": "https://github.com/Musare/MusareNode",

+ 22 - 52
frontend/App.vue

@@ -26,38 +26,26 @@ import WhatIsNew from "./components/Modals/WhatIsNew.vue";
 import MobileAlert from "./components/Modals/MobileAlert.vue";
 import LoginModal from "./components/Modals/Login.vue";
 import RegisterModal from "./components/Modals/Register.vue";
-import auth from "./auth";
 import io from "./io";
 
 export default {
 	replace: false,
 	data() {
 		return {
-			banned: false,
-			ban: {},
-			loggedIn: false,
-			role: "",
-			username: "",
-			userId: "",
 			serverDomain: "",
 			socketConnected: true
 		};
 	},
 	computed: mapState({
+		loggedIn: state => state.user.auth.loggedIn,
+		role: state => state.user.auth.role,
+		username: state => state.user.auth.username,
+		userId: state => state.user.auth.userId,
+		banned: state => state.user.auth.banned,
 		modals: state => state.modals.modals,
 		currentlyActive: state => state.modals.currentlyActive
 	}),
 	methods: {
-		logout() {
-			const _this = this;
-			_this.socket.emit("users.logout", result => {
-				if (result.status === "success") {
-					document.cookie =
-						"SID=;expires=Thu, 01 Jan 1970 00:00:01 GMT;";
-					window.location.reload();
-				} else Toast.methods.addToast(result.message, 4000);
-			});
-		},
 		submitOnEnter: (cb, event) => {
 			if (event.which === 13) cb();
 		},
@@ -73,49 +61,37 @@ export default {
 				this.closeCurrentModal();
 		};
 
-		const _this = this;
 		if (localStorage.getItem("github_redirect")) {
 			this.$router.go(localStorage.getItem("github_redirect"));
 			localStorage.removeItem("github_redirect");
 		}
-		auth.isBanned((banned, ban) => {
-			_this.ban = ban;
-			_this.banned = banned;
-		});
-		auth.getStatus((authenticated, role, username, userId) => {
-			_this.socket = window.socket;
-			_this.loggedIn = authenticated;
-			_this.role = role;
-			_this.username = username;
-			_this.userId = userId;
-		});
 		io.onConnect(true, () => {
-			_this.socketConnected = true;
+			this.socketConnected = true;
 		});
 		io.onConnectError(true, () => {
-			_this.socketConnected = false;
+			this.socketConnected = false;
 		});
 		io.onDisconnect(true, () => {
-			_this.socketConnected = false;
+			this.socketConnected = false;
 		});
 		lofig.get("serverDomain", res => {
-			_this.serverDomain = res;
+			this.serverDomain = res;
 		});
-		_this.$router.onReady(() => {
-			if (_this.$route.query.err) {
-				let { err } = _this.$route.query;
+		this.$router.onReady(() => {
+			if (this.$route.query.err) {
+				let { err } = this.$route.query;
 				err = err
 					.replace(new RegExp("<", "g"), "&lt;")
 					.replace(new RegExp(">", "g"), "&gt;");
-				_this.$router.push({ query: {} });
+				this.$router.push({ query: {} });
 				Toast.methods.addToast(err, 20000);
 			}
-			if (_this.$route.query.msg) {
-				let { msg } = _this.$route.query;
+			if (this.$route.query.msg) {
+				let { msg } = this.$route.query;
 				msg = msg
 					.replace(new RegExp("<", "g"), "&lt;")
 					.replace(new RegExp(">", "g"), "&gt;");
-				_this.$router.push({ query: {} });
+				this.$router.push({ query: {} });
 				Toast.methods.addToast(msg, 20000);
 			}
 		});
@@ -137,9 +113,7 @@ export default {
 </script>
 
 <style lang="scss">
-.center {
-	text-align: center;
-}
+@import "styles/global.scss";
 
 #toast-container {
 	z-index: 10000 !important;
@@ -163,8 +137,8 @@ html {
 
 .alert {
 	padding: 20px;
-	color: white;
-	background-color: red;
+	color: $white;
+	background-color: $red;
 	position: fixed;
 	top: 50px;
 	right: 50px;
@@ -183,9 +157,9 @@ html {
 		text-align: center;
 		padding: 7.5px 6px;
 		border-radius: 2px;
-		background-color: #323232;
+		background-color: $dark-grey;
 		font-size: 0.9em;
-		color: #fff;
+		color: $white;
 		content: attr(data-tooltip);
 		opacity: 0;
 		transition: all 0.2s ease-in-out 0.1s;
@@ -256,7 +230,7 @@ html {
 }
 .input:focus,
 .input:active {
-	border-color: #03a9f4 !important;
+	border-color: $primary-color !important;
 }
 button.delete:focus {
 	background-color: rgba(10, 10, 10, 0.3);
@@ -265,8 +239,4 @@ button.delete:focus {
 .tag {
 	padding-right: 6px !important;
 }
-
-.button.is-success {
-	background-color: #00b16a !important;
-}
 </style>

+ 16 - 0
frontend/api/auth.js

@@ -1,3 +1,4 @@
+import { Toast } from "vue-roaster";
 import io from "../io";
 
 // when Vuex needs to interact with socket.io
@@ -72,5 +73,20 @@ export default {
 				});
 			});
 		});
+	},
+	logout() {
+		return new Promise((resolve, reject) => {
+			io.getSocket(socket => {
+				socket.emit("users.logout", result => {
+					if (result.status === "success") {
+						document.cookie =
+							"SID=;expires=Thu, 01 Jan 1970 00:00:01 GMT;";
+						return window.location.reload();
+					}
+					Toast.methods.addToast(result.message, 4000);
+					return reject(new Error(result.message));
+				});
+			});
+		});
 	}
 };

+ 3 - 4
frontend/auth.js

@@ -17,11 +17,10 @@ export default {
 	},
 
 	setBanned(ban) {
-		const _this = this;
-		_this.banned = true;
-		_this.ban = ban;
+		this.banned = true;
+		this.ban = ban;
 		bannedCallbacks.forEach(callback => {
-			callback(true, _this.ban);
+			callback(true, this.ban);
 		});
 	},
 

+ 2 - 0
frontend/components/404.vue

@@ -8,6 +8,8 @@
 </template>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 * {
 	margin: 0;
 	padding: 0;

+ 36 - 39
frontend/components/Admin/EditStation.vue

@@ -163,9 +163,8 @@ export default {
 		editing: state => state.editing
 	}),
 	mounted() {
-		const _this = this;
 		io.getSocket(socket => {
-			_this.socket = socket;
+			this.socket = socket;
 			return socket;
 		});
 	},
@@ -264,8 +263,6 @@ export default {
 			);
 		},
 		updateDescription() {
-			const _this = this;
-
 			const { description } = this.editing;
 			if (!validation.isLength(description, 2, 200))
 				return Toast.methods.addToast(
@@ -288,13 +285,13 @@ export default {
 				description,
 				res => {
 					if (res.status === "success") {
-						if (_this.station) {
-							_this.station.description = description;
+						if (this.station) {
+							this.station.description = description;
 							return description;
 						}
-						_this.$parent.stations.forEach((station, index) => {
-							if (station._id === _this.editing._id) {
-								_this.$parent.stations[
+						this.$parent.stations.forEach((station, index) => {
+							if (station._id === this.editing._id) {
+								this.$parent.stations[
 									index
 								].description = description;
 								return description;
@@ -311,21 +308,21 @@ export default {
 			);
 		},
 		updatePrivacy() {
-			const _this = this;
 			this.socket.emit(
 				"stations.updatePrivacy",
 				this.editing._id,
 				this.editing.privacy,
 				res => {
 					if (res.status === "success") {
-						if (_this.station)
-							_this.station.privacy = _this.editing.privacy;
+						if (this.station)
+							this.station.privacy = this.editing.privacy;
 						else {
-							_this.$parent.stations.forEach((station, index) => {
-								if (station._id === _this.editing._id) {
-									_this.$parent.stations[index].privacy =
-										_this.editing.privacy;
-									return _this.editing.privacy;
+							this.$parent.stations.forEach((station, index) => {
+								if (station._id === this.editing._id) {
+									this.$parent.stations[
+										index
+									].privacy = this.editing.privacy;
+									return this.editing.privacy;
 								}
 
 								return false;
@@ -339,7 +336,6 @@ export default {
 			);
 		},
 		updateGenres() {
-			const _this = this;
 			this.socket.emit(
 				"stations.updateGenres",
 				this.editing._id,
@@ -347,12 +343,12 @@ export default {
 				res => {
 					if (res.status === "success") {
 						const genres = JSON.parse(
-							JSON.stringify(_this.editing.genres)
+							JSON.stringify(this.editing.genres)
 						);
-						if (_this.station) _this.station.genres = genres;
-						_this.$parent.stations.forEach((station, index) => {
-							if (station._id === _this.editing._id) {
-								_this.$parent.stations[index].genres = genres;
+						if (this.station) this.station.genres = genres;
+						this.$parent.stations.forEach((station, index) => {
+							if (station._id === this.editing._id) {
+								this.$parent.stations[index].genres = genres;
 								return genres;
 							}
 
@@ -366,7 +362,6 @@ export default {
 			);
 		},
 		updateBlacklistedGenres() {
-			const _this = this;
 			this.socket.emit(
 				"stations.updateBlacklistedGenres",
 				this.editing._id,
@@ -374,13 +369,13 @@ export default {
 				res => {
 					if (res.status === "success") {
 						const blacklistedGenres = JSON.parse(
-							JSON.stringify(_this.editing.blacklistedGenres)
+							JSON.stringify(this.editing.blacklistedGenres)
 						);
-						if (_this.station)
-							_this.station.blacklistedGenres = blacklistedGenres;
-						_this.$parent.stations.forEach((station, index) => {
-							if (station._id === _this.editing._id) {
-								_this.$parent.stations[
+						if (this.station)
+							this.station.blacklistedGenres = blacklistedGenres;
+						this.$parent.stations.forEach((station, index) => {
+							if (station._id === this.editing._id) {
+								this.$parent.stations[
 									index
 								].blacklistedGenres = blacklistedGenres;
 								return blacklistedGenres;
@@ -396,20 +391,20 @@ export default {
 			);
 		},
 		updatePartyMode() {
-			const _this = this;
 			this.socket.emit(
 				"stations.updatePartyMode",
 				this.editing._id,
 				this.editing.partyMode,
 				res => {
 					if (res.status === "success") {
-						if (_this.station)
-							_this.station.partyMode = _this.editing.partyMode;
-						_this.$parent.stations.forEach((station, index) => {
-							if (station._id === _this.editing._id) {
-								_this.$parent.stations[index].partyMode =
-									_this.editing.partyMode;
-								return _this.editing.partyMode;
+						if (this.station)
+							this.station.partyMode = this.editing.partyMode;
+						this.$parent.stations.forEach((station, index) => {
+							if (station._id === this.editing._id) {
+								this.$parent.stations[
+									index
+								].partyMode = this.editing.partyMode;
+								return this.editing.partyMode;
 							}
 
 							return false;
@@ -469,6 +464,8 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .controls {
 	display: flex;
 
@@ -493,6 +490,6 @@ h5 {
 }
 
 .select:after {
-	border-color: #029ce3;
+	border-color: $primary-color;
 }
 </style>

+ 22 - 38
frontend/components/Admin/News.vue

@@ -231,37 +231,36 @@ export default {
 				features: [],
 				improvements: [],
 				upcoming: []
-			},
-			editing: {}
+			}
 		};
 	},
 	mounted() {
-		const _this = this;
 		io.getSocket(socket => {
-			_this.socket = socket;
-			_this.socket.emit("news.index", res => {
-				_this.news = res.data;
+			this.socket = socket;
+			this.socket.emit("news.index", res => {
+				this.news = res.data;
 				return res.data;
 			});
-			_this.socket.on("event:admin.news.created", news => {
-				_this.news.unshift(news);
+			this.socket.on("event:admin.news.created", news => {
+				this.news.unshift(news);
 			});
-			_this.socket.on("event:admin.news.removed", news => {
-				_this.news = _this.news.filter(item => item._id !== news._id);
+			this.socket.on("event:admin.news.removed", news => {
+				this.news = this.news.filter(item => item._id !== news._id);
 			});
-			if (_this.socket.connected) _this.init();
-			io.onConnect(() => _this.init());
+			if (this.socket.connected) this.init();
+			io.onConnect(() => this.init());
 		});
 	},
 	computed: {
 		...mapState("modals", {
 			modals: state => state.modals.admin
+		}),
+		...mapState("admin/news", {
+			editing: state => state.editing
 		})
 	},
 	methods: {
 		createNews() {
-			const _this = this;
-
 			const {
 				creating: { bugs, features, improvements, upcoming }
 			} = this;
@@ -287,10 +286,10 @@ export default {
 					3000
 				);
 
-			return _this.socket.emit("news.create", _this.creating, result => {
+			return this.socket.emit("news.create", this.creating, result => {
 				Toast.methods.addToast(result.message, 4000);
 				if (result.status === "success")
-					_this.creating = {
+					this.creating = {
 						title: "",
 						description: "",
 						bugs: [],
@@ -306,27 +305,9 @@ export default {
 			);
 		},
 		editNews(news) {
-			this.editing = news;
+			this.editNews(news);
 			this.openModal({ sector: "admin", modal: "editNews" });
 		},
-		updateNews(close) {
-			const _this = this;
-			this.socket.emit(
-				"news.update",
-				_this.editing._id,
-				_this.editing,
-				res => {
-					Toast.methods.addToast(res.message, 4000);
-					if (res.status === "success") {
-						if (close)
-							_this.closeModal({
-								sector: "admin",
-								modal: "editNews"
-							});
-					}
-				}
-			);
-		},
 		addChange(type) {
 			const change = document.getElementById(`new-${type}`).value.trim();
 
@@ -346,12 +327,15 @@ export default {
 		init() {
 			this.socket.emit("apis.joinAdminRoom", "news", () => {});
 		},
-		...mapActions("modals", ["openModal", "closeModal"])
+		...mapActions("modals", ["openModal", "closeModal"]),
+		...mapActions("admin/news", ["editNews"])
 	}
 };
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .tag:not(:last-child) {
 	margin-right: 5px;
 }
@@ -361,10 +345,10 @@ td {
 }
 
 .is-info:focus {
-	background-color: #0398db;
+	background-color: $primary-color;
 }
 
 .card-footer-item {
-	color: #03a9f4;
+	color: $primary-color;
 }
 </style>

+ 12 - 13
frontend/components/Admin/Punishments.vue

@@ -142,39 +142,38 @@ export default {
 			this.openModal({ sector: "admin", modal: "viewPunishment" });
 		},
 		banIP() {
-			const _this = this;
-			_this.socket.emit(
+			this.socket.emit(
 				"punishments.banIP",
-				_this.ipBan.ip,
-				_this.ipBan.reason,
-				_this.ipBan.expiresAt,
+				this.ipBan.ip,
+				this.ipBan.reason,
+				this.ipBan.expiresAt,
 				res => {
 					Toast.methods.addToast(res.message, 6000);
 				}
 			);
 		},
 		init() {
-			const _this = this;
-			_this.socket.emit("punishments.index", res => {
-				if (res.status === "success") _this.punishments = res.data;
+			this.socket.emit("punishments.index", res => {
+				if (res.status === "success") this.punishments = res.data;
 			});
-			// _this.socket.emit('apis.joinAdminRoom', 'punishments', () => {});
+			// this.socket.emit('apis.joinAdminRoom', 'punishments', () => {});
 		},
 		...mapActions("modals", ["openModal"]),
 		...mapActions("admin/punishments", ["viewPunishment"])
 	},
 	mounted() {
-		const _this = this;
 		io.getSocket(socket => {
-			_this.socket = socket;
-			if (_this.socket.connected) _this.init();
-			io.onConnect(() => _this.init());
+			this.socket = socket;
+			if (this.socket.connected) this.init();
+			io.onConnect(() => this.init());
 		});
 	}
 };
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 body {
 	font-family: "Roboto", sans-serif;
 }

+ 39 - 37
frontend/components/Admin/QueueSongs.vue

@@ -14,10 +14,9 @@
 					<tr>
 						<td>Thumbnail</td>
 						<td>Title</td>
-						<td>ID</td>
-						<td>YouTube ID</td>
 						<td>Artists</td>
 						<td>Genres</td>
+						<td>ID / YouTube ID</td>
 						<td>Requested By</td>
 						<td>Options</td>
 					</tr>
@@ -34,8 +33,11 @@
 						<td>
 							<strong>{{ song.title }}</strong>
 						</td>
-						<td>{{ song._id }}</td>
+						<td>{{ song.artists.join(", ") }}</td>
+						<td>{{ song.genres.join(", ") }}</td>
 						<td>
+							{{ song._id }}
+							<br />
 							<a
 								:href="
 									'https://www.youtube.com/watch?v=' +
@@ -46,8 +48,6 @@
 								{{ song.songId }}</a
 							>
 						</td>
-						<td>{{ song.artists.join(", ") }}</td>
-						<td>{{ song.genres.join(", ") }}</td>
 						<td>
 							<user-id-to-username
 								:userId="song.requestedBy"
@@ -122,8 +122,12 @@ export default {
 	},
 	computed: {
 		filteredSongs() {
-			return this.songs;
-			// return this.songs.filter(song => song.indexOf(song.searchQuery) !== -1);
+			return this.songs.filter(
+				song =>
+					JSON.stringify(Object.values(song)).indexOf(
+						this.searchQuery
+					) !== -1
+			);
 		},
 		...mapState("modals", {
 			modals: state => state.modals.admin
@@ -137,9 +141,8 @@ export default {
 	// },
 	methods: {
 		getSet(position) {
-			const _this = this;
 			this.socket.emit("queueSongs.getSet", position, data => {
-				_this.songs = data;
+				this.songs = data;
 				this.position = position;
 			});
 		},
@@ -167,44 +170,41 @@ export default {
 			});
 		},
 		init() {
-			const _this = this;
-			_this.socket.emit("queueSongs.index", data => {
-				_this.songs = data.songs;
-				_this.maxPosition = Math.round(data.maxLength / 50);
+			this.socket.emit("queueSongs.index", data => {
+				this.songs = data.songs;
+				this.maxPosition = Math.round(data.maxLength / 50);
 			});
-			_this.socket.emit("apis.joinAdminRoom", "queue", () => {});
+			this.socket.emit("apis.joinAdminRoom", "queue", () => {});
 		},
 		...mapActions("admin/songs", ["stopVideo", "editSong"]),
 		...mapActions("modals", ["openModal"])
 	},
 	mounted() {
-		const _this = this;
 		io.getSocket(socket => {
-			_this.socket = socket;
-			if (_this.socket.connected) {
-				_this.init();
-				_this.socket.on("event:admin.queueSong.added", queueSong => {
-					_this.songs.push(queueSong);
-				});
-				_this.socket.on("event:admin.queueSong.removed", songId => {
-					_this.songs = _this.songs.filter(song => {
-						return song._id !== songId;
-					});
+			this.socket = socket;
+
+			this.socket.on("event:admin.queueSong.added", queueSong => {
+				this.songs.push(queueSong);
+			});
+			this.socket.on("event:admin.queueSong.removed", songId => {
+				this.songs = this.songs.filter(song => {
+					return song._id !== songId;
 				});
-				_this.socket.on(
-					"event:admin.queueSong.updated",
-					updatedSong => {
-						for (let i = 0; i < _this.songs.length; i += 1) {
-							const song = _this.songs[i];
-							if (song._id === updatedSong._id) {
-								_this.songs.$set(i, updatedSong);
-							}
-						}
+			});
+			this.socket.on("event:admin.queueSong.updated", updatedSong => {
+				for (let i = 0; i < this.songs.length; i += 1) {
+					const song = this.songs[i];
+					if (song._id === updatedSong._id) {
+						this.songs.$set(i, updatedSong);
 					}
-				);
+				}
+			});
+
+			if (this.socket.connected) {
+				this.init();
 			}
 			io.onConnect(() => {
-				_this.init();
+				this.init();
 			});
 		});
 	}
@@ -212,6 +212,8 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .optionsColumn {
 	width: 140px;
 	button {
@@ -230,6 +232,6 @@ td {
 }
 
 .is-primary:focus {
-	background-color: #029ce3 !important;
+	background-color: $primary-color !important;
 }
 </style>

+ 13 - 13
frontend/components/Admin/Reports.vue

@@ -68,29 +68,28 @@ export default {
 		};
 	},
 	mounted() {
-		const _this = this;
 		io.getSocket(socket => {
-			_this.socket = socket;
-			if (_this.socket.connected) _this.init();
-			_this.socket.emit("reports.index", res => {
-				_this.reports = res.data;
+			this.socket = socket;
+			if (this.socket.connected) this.init();
+			this.socket.emit("reports.index", res => {
+				this.reports = res.data;
 			});
-			_this.socket.on("event:admin.report.resolved", reportId => {
-				_this.reports = _this.reports.filter(report => {
+			this.socket.on("event:admin.report.resolved", reportId => {
+				this.reports = this.reports.filter(report => {
 					return report._id !== reportId;
 				});
 			});
-			_this.socket.on("event:admin.report.created", report => {
-				_this.reports.push(report);
+			this.socket.on("event:admin.report.created", report => {
+				this.reports.push(report);
 			});
 			io.onConnect(() => {
-				_this.init();
+				this.init();
 			});
 		});
 
 		if (this.$route.query.id) {
 			this.socket.emit("reports.findOne", this.$route.query.id, res => {
-				if (res.status === "success") _this.view(res.data);
+				if (res.status === "success") this.view(res.data);
 				else
 					Toast.methods.addToast(
 						"Report with that ID not found",
@@ -113,11 +112,10 @@ export default {
 			this.openModal({ sector: "admin", modal: "viewReport" });
 		},
 		resolve(reportId) {
-			const _this = this;
 			this.socket.emit("reports.resolve", reportId, res => {
 				Toast.methods.addToast(res.message, 3000);
 				if (res.status === "success" && this.modals.viewReport)
-					_this.closeModal({
+					this.closeModal({
 						sector: "admin",
 						modal: "viewReport"
 					});
@@ -130,6 +128,8 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .tag:not(:last-child) {
 	margin-right: 5px;
 }

+ 70 - 42
frontend/components/Admin/Songs.vue

@@ -14,10 +14,17 @@
 					<tr>
 						<td>Thumbnail</td>
 						<td>Title</td>
-						<td>ID</td>
-						<td>YouTube ID</td>
 						<td>Artists</td>
 						<td>Genres</td>
+						<td class="likesColumn">
+							<i class="material-icons thumbLike">thumb_up</i>
+						</td>
+						<td class="dislikesColumn">
+							<i class="material-icons thumbDislike"
+								>thumb_down</i
+							>
+						</td>
+						<td>ID / Youtube ID</td>
 						<td>Requested By</td>
 						<td>Options</td>
 					</tr>
@@ -34,8 +41,13 @@
 						<td>
 							<strong>{{ song.title }}</strong>
 						</td>
-						<td>{{ song._id }}</td>
+						<td>{{ song.artists.join(", ") }}</td>
+						<td>{{ song.genres.join(", ") }}</td>
+						<td>{{ song.likes }}</td>
+						<td>{{ song.dislikes }}</td>
 						<td>
+							{{ song._id }}
+							<br />
 							<a
 								:href="
 									'https://www.youtube.com/watch?v=' +
@@ -46,8 +58,6 @@
 								{{ song.songId }}</a
 							>
 						</td>
-						<td>{{ song.artists.join(", ") }}</td>
-						<td>{{ song.genres.join(", ") }}</td>
 						<td>
 							<user-id-to-username
 								:userId="song.requestedBy"
@@ -92,7 +102,6 @@ export default {
 		return {
 			position: 1,
 			maxPosition: 1,
-			songs: [],
 			searchQuery: "",
 			editing: {
 				index: 0,
@@ -102,11 +111,18 @@ export default {
 	},
 	computed: {
 		filteredSongs() {
-			return this.songs;
-			// return this.songs.filter(song => song.indexOf(song.searchQuery) !== -1);
+			return this.songs.filter(
+				song =>
+					JSON.stringify(Object.values(song)).indexOf(
+						this.searchQuery
+					) !== -1
+			);
 		},
 		...mapState("modals", {
 			modals: state => state.modals.admin
+		}),
+		...mapState("admin/songs", {
+			songs: state => state.songs
 		})
 	},
 	watch: {
@@ -127,52 +143,48 @@ export default {
 			});
 		},
 		getSet() {
-			const _this = this;
-			_this.socket.emit("songs.getSet", _this.position, data => {
+			this.socket.emit("songs.getSet", this.position, data => {
 				data.forEach(song => {
-					_this.songs.push(song);
+					this.addSong(song);
 				});
-				_this.position += 1;
-				if (_this.maxPosition > _this.position - 1) _this.getSet();
+				this.position += 1;
+				if (this.maxPosition > this.position - 1) this.getSet();
 			});
 		},
 		init() {
-			const _this = this;
-			_this.songs = [];
-			_this.socket.emit("songs.length", length => {
-				_this.maxPosition = Math.ceil(length / 15);
-				_this.getSet();
+			this.socket.emit("songs.length", length => {
+				this.maxPosition = Math.ceil(length / 15);
+				this.getSet();
 			});
-			_this.socket.emit("apis.joinAdminRoom", "songs", () => {});
+			this.socket.emit("apis.joinAdminRoom", "songs", () => {});
 		},
-		...mapActions("admin/songs", ["stopVideo", "editSong"]),
+		...mapActions("admin/songs", [
+			"stopVideo",
+			"editSong",
+			"addSong",
+			"removeSong",
+			"updateSong"
+		]),
 		...mapActions("modals", ["openModal", "closeModal"])
 	},
 	mounted() {
-		const _this = this;
 		io.getSocket(socket => {
-			_this.socket = socket;
-			if (_this.socket.connected) {
-				_this.init();
-				_this.socket.on("event:admin.song.added", song => {
-					_this.songs.push(song);
-				});
-				_this.socket.on("event:admin.song.removed", songId => {
-					_this.songs = _this.songs.filter(song => {
-						return song._id !== songId;
-					});
-				});
-				_this.socket.on("event:admin.song.updated", updatedSong => {
-					for (let i = 0; i < _this.songs.length; i += 1) {
-						const song = _this.songs[i];
-						if (song._id === updatedSong._id) {
-							_this.songs.$set(i, updatedSong);
-						}
-					}
-				});
+			this.socket = socket;
+			this.socket.on("event:admin.song.added", song => {
+				this.addSong(song);
+			});
+			this.socket.on("event:admin.song.removed", songId => {
+				this.removeSong(songId);
+			});
+			this.socket.on("event:admin.song.updated", updatedSong => {
+				this.updateSong(updatedSong);
+			});
+
+			if (this.socket.connected) {
+				this.init();
 			}
 			io.onConnect(() => {
-				_this.init();
+				this.init();
 			});
 		});
 
@@ -190,6 +202,8 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 body {
 	font-family: "Roboto", sans-serif;
 }
@@ -201,6 +215,20 @@ body {
 	}
 }
 
+.likesColumn,
+.dislikesColumn {
+	width: 40px;
+	i {
+		font-size: 20px;
+	}
+	.thumbLike {
+		color: $green !important;
+	}
+	.thumbDislike {
+		color: $red !important;
+	}
+}
+
 .song-thumbnail {
 	display: block;
 	max-width: 50px;
@@ -212,6 +240,6 @@ td {
 }
 
 .is-primary:focus {
-	background-color: #029ce3 !important;
+	background-color: $primary-color !important;
 }
 </style>

+ 31 - 17
frontend/components/Admin/Stations.vue

@@ -19,7 +19,16 @@
 							<span>{{ station._id }}</span>
 						</td>
 						<td>
-							<span>{{ station.name }}</span>
+							<span>
+								<router-link
+									:to="{
+										name: 'station',
+										params: { id: station.name }
+									}"
+								>
+									{{ station.name }}
+								</router-link>
+							</span>
 						</td>
 						<td>
 							<span>{{ station.type }}</span>
@@ -31,7 +40,13 @@
 							<span>{{ station.description }}</span>
 						</td>
 						<td>
+							<span
+								v-if="station.type === 'official'"
+								title="Musare"
+								>Musare</span
+							>
 							<user-id-to-username
+								v-else
 								:userId="station.owner"
 								:link="true"
 							/>
@@ -192,7 +207,6 @@ export default {
 	},
 	methods: {
 		createStation() {
-			const _this = this;
 			const {
 				newStation: {
 					name,
@@ -219,7 +233,7 @@ export default {
 					3000
 				);
 
-			return _this.socket.emit(
+			return this.socket.emit(
 				"stations.create",
 				{
 					name,
@@ -301,30 +315,28 @@ export default {
 			this.newStation.blacklistedGenres.splice(index, 1);
 		},
 		init() {
-			const _this = this;
-			_this.socket.emit("stations.index", data => {
-				_this.stations = data.stations;
+			this.socket.emit("stations.index", data => {
+				this.stations = data.stations;
 			});
-			_this.socket.emit("apis.joinAdminRoom", "stations", () => {});
+			this.socket.emit("apis.joinAdminRoom", "stations", () => {});
 		},
 		...mapActions("modals", ["openModal"]),
 		...mapActions("admin/stations", ["editStation"])
 	},
 	mounted() {
-		const _this = this;
 		io.getSocket(socket => {
-			_this.socket = socket;
-			if (_this.socket.connected) _this.init();
-			_this.socket.on("event:admin.station.added", station => {
-				_this.stations.push(station);
+			this.socket = socket;
+			if (this.socket.connected) this.init();
+			this.socket.on("event:admin.station.added", station => {
+				this.stations.push(station);
 			});
-			_this.socket.on("event:admin.station.removed", stationId => {
-				_this.stations = _this.stations.filter(station => {
+			this.socket.on("event:admin.station.removed", stationId => {
+				this.stations = this.stations.filter(station => {
 					return station._id !== stationId;
 				});
 			});
 			io.onConnect(() => {
-				_this.init();
+				this.init();
 			});
 		});
 	}
@@ -332,6 +344,8 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .tag {
 	margin-top: 5px;
 	&:not(:last-child) {
@@ -346,7 +360,7 @@ td {
 }
 
 .is-info:focus {
-	background-color: #0398db;
+	background-color: $primary-color;
 }
 
 .genre-wrapper {
@@ -355,6 +369,6 @@ td {
 }
 
 .card-footer-item {
-	color: #029ce3;
+	color: $primary-color;
 }
 </style>

+ 8 - 7
frontend/components/Admin/Statistics.vue

@@ -144,11 +144,10 @@ export default {
 		};
 	},
 	mounted() {
-		const _this = this;
 		const minuteCtx = document.getElementById("minuteChart");
 		const hourCtx = document.getElementById("hourChart");
 
-		_this.minuteChart = new Chart(minuteCtx, {
+		this.minuteChart = new Chart(minuteCtx, {
 			type: "line",
 			data: {
 				labels: [
@@ -207,7 +206,7 @@ export default {
 			}
 		});
 
-		_this.hourChart = new Chart(hourCtx, {
+		this.hourChart = new Chart(hourCtx, {
 			type: "line",
 			data: {
 				labels: [
@@ -267,9 +266,9 @@ export default {
 		});
 
 		io.getSocket(socket => {
-			_this.socket = socket;
-			if (_this.socket.connected) _this.init();
-			io.onConnect(() => _this.init());
+			this.socket = socket;
+			if (this.socket.connected) this.init();
+			io.onConnect(() => this.init());
 		});
 	},
 	methods: {
@@ -329,6 +328,8 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 body {
 	font-family: "Roboto", sans-serif;
 }
@@ -344,6 +345,6 @@ td {
 }
 
 .is-primary:focus {
-	background-color: #029ce3 !important;
+	background-color: $primary-color !important;
 }
 </style>

+ 9 - 12
frontend/components/Admin/Users.vue

@@ -86,30 +86,27 @@ export default {
 			this.openModal({ sector: "admin", modal: "editUser" });
 		},
 		init() {
-			const _this = this;
-			_this.socket.emit("users.index", result => {
-				if (result.status === "success") _this.users = result.data;
-			});
-			_this.socket.emit("apis.joinAdminRoom", "users", () => {});
-			_this.socket.on("event:user.username.changed", username => {
-				_this.$parent.$parent.username = username;
+			this.socket.emit("users.index", result => {
+				if (result.status === "success") this.users = result.data;
 			});
+			this.socket.emit("apis.joinAdminRoom", "users", () => {});
 		},
 		...mapActions("admin/users", ["editUser"]),
 		...mapActions("modals", ["openModal"])
 	},
 	mounted() {
-		const _this = this;
 		io.getSocket(socket => {
-			_this.socket = socket;
-			if (_this.socket.connected) _this.init();
-			io.onConnect(() => _this.init());
+			this.socket = socket;
+			if (this.socket.connected) this.init();
+			io.onConnect(() => this.init());
 		});
 	}
 };
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 body {
 	font-family: "Roboto", sans-serif;
 }
@@ -125,6 +122,6 @@ td {
 }
 
 .is-primary:focus {
-	background-color: #029ce3 !important;
+	background-color: $primary-color !important;
 }
 </style>

+ 6 - 4
frontend/components/MainFooter.vue

@@ -82,6 +82,8 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .content a:not(.button) {
 	border: 0;
 }
@@ -102,7 +104,7 @@ export default {
 	border-radius: 33% 33% 0% 0% / 7% 7% 0% 0%;
 	box-shadow: 0 4px 8px 0 rgba(3, 169, 244, 0.65),
 		0 6px 20px 0 rgba(3, 169, 244, 0.4);
-	background-color: #ffffff;
+	background-color: $white;
 	width: 100%;
 
 	.musareFooterLogo {
@@ -123,15 +125,15 @@ export default {
 
 	.footerLinks {
 		:not(:last-child) {
-			border-right: solid 1px #03a9f4;
+			border-right: solid 1px $primary-color;
 		}
 		a {
 			padding: 0 5px;
 			font-size: 18px;
-			color: #03a9f4;
+			color: $primary-color;
 		}
 		a:hover {
-			color: #03a9f4;
+			color: $primary-color;
 			text-decoration: underline;
 		}
 	}

+ 22 - 20
frontend/components/MainHeader.vue

@@ -21,18 +21,18 @@
 
 		<div class="nav-right nav-menu" :class="{ 'is-active': isMobile }">
 			<router-link
-				v-if="$parent.$parent.role === 'admin'"
+				v-if="role === 'admin'"
 				class="nav-item is-tab admin"
 				to="/admin"
 			>
 				<strong>Admin</strong>
 			</router-link>
-			<span v-if="$parent.$parent.loggedIn" class="grouped">
+			<span v-if="loggedIn" class="grouped">
 				<router-link
 					class="nav-item is-tab"
 					:to="{
 						name: 'profile',
-						params: { username: $parent.$parent.username }
+						params: { username }
 					}"
 				>
 					Profile
@@ -40,12 +40,7 @@
 				<router-link class="nav-item is-tab" to="/settings"
 					>Settings</router-link
 				>
-				<a
-					class="nav-item is-tab"
-					href="#"
-					@click="$parent.$parent.logout()"
-					>Logout</a
-				>
+				<a class="nav-item is-tab" href="#" @click="logout()">Logout</a>
 			</span>
 			<span v-else class="grouped">
 				<a
@@ -99,41 +94,48 @@ export default {
 			return res;
 		});
 	},
-	computed: mapState("modals", {
-		modals: state => state.modals.header
+	computed: mapState({
+		modals: state => state.modals.modals.header,
+		role: state => state.user.auth.role,
+		loggedIn: state => state.user.auth.loggedIn,
+		username: state => state.user.auth.username
 	}),
 	methods: {
-		...mapActions("modals", ["openModal"])
+		...mapActions("modals", ["openModal"]),
+		...mapActions("user/auth", ["logout"])
 	}
 };
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .nav {
-	background-color: #03a9f4;
+	background-color: $primary-color;
 	height: 64px;
 	border-radius: 0% 0% 33% 33% / 0% 0% 7% 7%;
 
 	.nav-menu.is-active {
 		.nav-item {
-			color: #333;
+			color: $dark-grey-2;
 
 			&:hover {
-				color: #333;
+				color: $dark-grey-2;
 			}
 		}
 	}
 
 	a.nav-item.is-tab:hover {
 		border-bottom: none;
-		border-top: solid 1px #ffffff;
+		border-top: solid 1px $white;
+		padding-top: 9px;
 	}
 
 	.nav-toggle {
 		height: 64px;
 
 		&.is-active span {
-			background-color: #333;
+			background-color: $dark-grey-2;
 		}
 	}
 
@@ -141,7 +143,7 @@ export default {
 		font-size: 2.1rem !important;
 		line-height: 64px !important;
 		padding: 0 20px;
-		color: #ffffff;
+		color: $white;
 		font-family: Pacifico, cursive;
 		filter: brightness(0) invert(1);
 
@@ -152,10 +154,10 @@ export default {
 
 	.nav-item {
 		font-size: 17px;
-		color: #ffffff;
+		color: $white;
 
 		&:hover {
-			color: #ffffff;
+			color: $white;
 		}
 	}
 	.admin strong {

+ 35 - 34
frontend/components/Modals/AddSongToPlaylist.vue

@@ -2,10 +2,10 @@
 	<modal title="Add Song To Playlist">
 		<template v-slot:body>
 			<h4 class="songTitle">
-				{{ $parent.currentSong.title }}
+				{{ currentSong.title }}
 			</h4>
 			<h5 class="songArtist">
-				{{ $parent.currentSong.artists }}
+				{{ currentSong.artists }}
 			</h5>
 			<aside class="menu">
 				<p class="menu-label">
@@ -38,6 +38,8 @@
 </template>
 
 <script>
+import { mapState } from "vuex";
+
 import { Toast } from "vue-roaster";
 import Modal from "./Modal.vue";
 import io from "../../io";
@@ -53,84 +55,83 @@ export default {
 		};
 	},
 	mounted() {
-		const _this = this;
-		this.songId = this.$parent.currentSong.songId;
-		this.song = this.$parent.currentSong;
+		this.songId = this.currentSong.songId;
+		this.song = this.currentSong;
 		io.getSocket(socket => {
-			_this.socket = socket;
-			_this.socket.emit("playlists.indexForUser", res => {
+			this.socket = socket;
+			this.socket.emit("playlists.indexForUser", res => {
 				if (res.status === "success") {
 					res.data.forEach(playlist => {
-						_this.playlists[playlist._id] = playlist;
+						this.playlists[playlist._id] = playlist;
 					});
-					_this.recalculatePlaylists();
+					this.recalculatePlaylists();
 				}
 			});
 		});
 	},
+	computed: {
+		...mapState("station", {
+			currentSong: state => state.currentSong
+		})
+	},
 	methods: {
 		addSongToPlaylist(playlistId) {
-			const _this = this;
 			this.socket.emit(
 				"playlists.addSongToPlaylist",
-				this.$parent.currentSong.songId,
+				this.currentSong.songId,
 				playlistId,
 				res => {
 					Toast.methods.addToast(res.message, 4000);
 					if (res.status === "success") {
-						_this.playlists[playlistId].songs.push(_this.song);
+						this.playlists[playlistId].songs.push(this.song);
 					}
-					_this.recalculatePlaylists();
-					// this.$parent.modals.addSongToPlaylist = false;
+					this.recalculatePlaylists();
 				}
 			);
 		},
 		removeSongFromPlaylist(playlistId) {
-			const _this = this;
 			this.socket.emit(
 				"playlists.removeSongFromPlaylist",
-				_this.songId,
+				this.songId,
 				playlistId,
 				res => {
 					Toast.methods.addToast(res.message, 4000);
 					if (res.status === "success") {
-						_this.playlists[playlistId].songs.forEach(
+						this.playlists[playlistId].songs.forEach(
 							(song, index) => {
-								if (song.songId === _this.songId)
-									_this.playlists[playlistId].songs.splice(
+								if (song.songId === this.songId)
+									this.playlists[playlistId].songs.splice(
 										index,
 										1
 									);
 							}
 						);
 					}
-					_this.recalculatePlaylists();
-					// this.$parent.modals.addSongToPlaylist = false;
+					this.recalculatePlaylists();
 				}
 			);
 		},
 		recalculatePlaylists() {
-			const _this = this;
-			_this.playlistsArr = Object.values(_this.playlists).map(
-				playlist => {
-					let hasSong = false;
-					for (let i = 0; i < playlist.songs.length; i += 1) {
-						if (playlist.songs[i].songId === _this.songId) {
-							hasSong = true;
-						}
+			this.playlistsArr = Object.values(this.playlists).map(playlist => {
+				let hasSong = false;
+				for (let i = 0; i < playlist.songs.length; i += 1) {
+					if (playlist.songs[i].songId === this.songId) {
+						hasSong = true;
 					}
-
-					playlist.hasSong = hasSong; // eslint-disable-line no-param-reassign
-					_this.playlists[playlist._id] = playlist;
-					return playlist;
 				}
-			);
+
+				playlist.hasSong = hasSong; // eslint-disable-line no-param-reassign
+				this.playlists[playlist._id] = playlist;
+				return playlist;
+			});
 		}
 	}
 };
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .icon.is-small {
 	margin-right: 10px !important;
 }

+ 34 - 34
frontend/components/Modals/AddSongToQueue.vue

@@ -1,16 +1,13 @@
 <template>
 	<modal title="Add Song To Queue">
 		<div slot="body">
-			<aside
-				class="menu"
-				v-if="$parent.$parent.loggedIn && $parent.type === 'community'"
-			>
+			<aside class="menu" v-if="loggedIn && station.type === 'community'">
 				<ul class="menu-list">
 					<li v-for="(playlist, index) in playlists" :key="index">
 						<a
 							href="#"
 							target="_blank"
-							v-on:click="$parent.editPlaylist(playlist._id)"
+							v-on:click="editPlaylist(playlist._id)"
 							>{{ playlist.displayName }}</a
 						>
 						<div class="controls">
@@ -53,7 +50,7 @@
 					>
 				</p>
 			</div>
-			<div class="control is-grouped" v-if="$parent.type === 'official'">
+			<div class="control is-grouped" v-if="station.type === 'official'">
 				<p class="control is-expanded">
 					<input
 						class="input"
@@ -95,6 +92,8 @@
 </template>
 
 <script>
+import { mapState, mapActions } from "vuex";
+
 import { Toast } from "vue-roaster";
 import Modal from "./Modal.vue";
 import io from "../../io";
@@ -109,31 +108,33 @@ export default {
 			importQuery: ""
 		};
 	},
+	computed: mapState({
+		loggedIn: state => state.user.auth.loggedIn,
+		station: state => state.station.station
+	}),
 	methods: {
 		isPlaylistSelected(playlistId) {
 			return this.privatePlaylistQueueSelected === playlistId;
 		},
 		selectPlaylist(playlistId) {
-			const _this = this;
-			if (_this.$parent.type === "community") {
-				_this.privatePlaylistQueueSelected = playlistId;
-				_this.$parent.privatePlaylistQueueSelected = playlistId;
-				_this.$parent.addFirstPrivatePlaylistSongToQueue();
+			if (this.station.type === "community") {
+				this.privatePlaylistQueueSelected = playlistId;
+				this.$parent.privatePlaylistQueueSelected = playlistId;
+				this.$parent.addFirstPrivatePlaylistSongToQueue();
 			}
 		},
 		unSelectPlaylist() {
-			const _this = this;
-			if (_this.$parent.type === "community") {
-				_this.privatePlaylistQueueSelected = null;
-				_this.$parent.privatePlaylistQueueSelected = null;
+			if (this.station.type === "community") {
+				this.privatePlaylistQueueSelected = null;
+				this.$parent.privatePlaylistQueueSelected = null;
 			}
 		},
 		addSongToQueue(songId) {
-			const _this = this;
-			if (_this.$parent.type === "community") {
-				_this.socket.emit(
+			console.log(this.station.type);
+			if (this.station.type === "community") {
+				this.socket.emit(
 					"stations.addToQueue",
-					_this.$parent.station._id,
+					this.station._id,
 					songId,
 					data => {
 						if (data.status !== "success")
@@ -145,7 +146,7 @@ export default {
 					}
 				);
 			} else {
-				_this.socket.emit("queueSongs.add", songId, data => {
+				this.socket.emit("queueSongs.add", songId, data => {
 					if (data.status !== "success")
 						Toast.methods.addToast(`Error: ${data.message}`, 8000);
 					else Toast.methods.addToast(`${data.message}`, 4000);
@@ -153,22 +154,20 @@ export default {
 			}
 		},
 		importPlaylist() {
-			const _this = this;
 			Toast.methods.addToast(
 				"Starting to import your playlist. This can take some time to do.",
 				4000
 			);
 			this.socket.emit(
 				"queueSongs.addSetToQueue",
-				_this.importQuery,
+				this.importQuery,
 				res => {
 					Toast.methods.addToast(res.message, 4000);
 				}
 			);
 		},
 		submitQuery() {
-			const _this = this;
-			let query = _this.querySearch;
+			let query = this.querySearch;
 			if (query.indexOf("&index=") !== -1) {
 				query = query.split("&index=");
 				query.pop();
@@ -179,12 +178,12 @@ export default {
 				query.pop();
 				query = query.join("");
 			}
-			_this.socket.emit("apis.searchYoutube", query, res => {
+			this.socket.emit("apis.searchYoutube", query, res => {
 				// check for error
 				const { data } = res;
-				_this.queryResults = [];
+				this.queryResults = [];
 				for (let i = 0; i < data.items.length; i += 1) {
-					_this.queryResults.push({
+					this.queryResults.push({
 						id: data.items[i].id.videoId,
 						url: `https://www.youtube.com/watch?v=${this.id}`,
 						title: data.items[i].snippet.title,
@@ -192,17 +191,16 @@ export default {
 					});
 				}
 			});
-		}
+		},
+		...mapActions("user/playlists", ["editPlaylist"])
 	},
 	mounted() {
-		const _this = this;
 		io.getSocket(socket => {
-			_this.socket = socket;
-			_this.socket.emit("playlists.indexForUser", res => {
-				if (res.status === "success") _this.playlists = res.data;
+			this.socket = socket;
+			this.socket.emit("playlists.indexForUser", res => {
+				if (res.status === "success") this.playlists = res.data;
 			});
-			_this.privatePlaylistQueueSelected =
-				_this.$parent.privatePlaylistQueueSelected;
+			this.privatePlaylistQueueSelected = this.$parent.privatePlaylistQueueSelected;
 		});
 	},
 	components: { Modal }
@@ -210,6 +208,8 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 tr td {
 	vertical-align: middle;
 

+ 2 - 5
frontend/components/Modals/CreateCommunityStation.vue

@@ -58,9 +58,8 @@ export default {
 		};
 	},
 	mounted() {
-		const _this = this;
 		io.getSocket(socket => {
-			_this.socket = socket;
+			this.socket = socket;
 		});
 	},
 	methods: {
@@ -114,8 +113,6 @@ export default {
 					8000
 				);
 
-			const _this = this;
-
 			return this.socket.emit(
 				"stations.create",
 				{
@@ -130,7 +127,7 @@ export default {
 							`You have added the station successfully`,
 							4000
 						);
-						_this.closeModal({
+						this.closeModal({
 							sector: "home",
 							modal: "createCommunityStation"
 						});

+ 41 - 25
frontend/components/Modals/EditNews.vue

@@ -4,7 +4,7 @@
 			<label class="label">Title</label>
 			<p class="control">
 				<input
-					v-model="$parent.editing.title"
+					v-model="editing.title"
 					class="input"
 					type="text"
 					placeholder="News Title"
@@ -14,7 +14,7 @@
 			<label class="label">Description</label>
 			<p class="control">
 				<input
-					v-model="$parent.editing.description"
+					v-model="editing.description"
 					class="input"
 					type="text"
 					placeholder="News Description"
@@ -39,7 +39,7 @@
 						>
 					</p>
 					<span
-						v-for="(bug, index) in $parent.editing.bugs"
+						v-for="(bug, index) in editing.bugs"
 						class="tag is-info"
 						:key="index"
 					>
@@ -68,7 +68,7 @@
 						>
 					</p>
 					<span
-						v-for="(feature, index) in $parent.editing.features"
+						v-for="(feature, index) in editing.features"
 						class="tag is-info"
 						:key="index"
 					>
@@ -100,8 +100,7 @@
 						>
 					</p>
 					<span
-						v-for="(improvement, index) in $parent.editing
-							.improvements"
+						v-for="(improvement, index) in editing.improvements"
 						class="tag is-info"
 						:key="index"
 					>
@@ -130,7 +129,7 @@
 						>
 					</p>
 					<span
-						v-for="(upcoming, index) in $parent.editing.upcoming"
+						v-for="(upcoming, index) in editing.upcoming"
 						class="tag is-info"
 						:key="index"
 					>
@@ -144,14 +143,11 @@
 			</div>
 		</div>
 		<div slot="footer">
-			<button
-				class="button is-success"
-				@click="$parent.updateNews(false)"
-			>
+			<button class="button is-success" @click="updateNews(false)">
 				<i class="material-icons save-changes">done</i>
 				<span>&nbsp;Save</span>
 			</button>
-			<button class="button is-success" @click="$parent.updateNews(true)">
+			<button class="button is-success" @click="updateNews(true)">
 				<i class="material-icons save-changes">done</i>
 				<span>&nbsp;Save and close</span>
 			</button>
@@ -183,24 +179,44 @@ export default {
 		addChange(type) {
 			const change = document.getElementById(`edit-${type}`).value.trim();
 
-			if (this.$parent.editing[type].indexOf(change) !== -1)
+			if (this.editing[type].indexOf(change) !== -1)
 				return Toast.methods.addToast(`Tag already exists`, 3000);
 
-			if (change) this.$parent.editing[type].push(change);
+			if (change) this.addChange(type, change);
 			else Toast.methods.addToast(`${type} cannot be empty`, 3000);
 
 			document.getElementById(`edit-${type}`).value = "";
 			return true;
 		},
 		removeChange(type, index) {
-			this.$parent.editing[type].splice(index, 1);
+			this.removeChange(type, index);
 		},
-		...mapActions("modals", ["closeModal"])
+		updateNews(close) {
+			this.socket.emit(
+				"news.update",
+				this.editing._id,
+				this.editing,
+				res => {
+					Toast.methods.addToast(res.message, 4000);
+					if (res.status === "success") {
+						if (close)
+							this.closeModal({
+								sector: "admin",
+								modal: "editNews"
+							});
+					}
+				}
+			);
+		},
+		...mapActions("modals", ["closeModal"]),
+		...mapActions("admin/users", ["addChange", "removeChange"])
 	}
 };
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 input[type="range"] {
 	-webkit-appearance: none;
 	width: 100%;
@@ -216,7 +232,7 @@ input[type="range"]::-webkit-slider-runnable-track {
 	height: 5.2px;
 	cursor: pointer;
 	box-shadow: 0;
-	background: #c2c0c2;
+	background: $light-grey-2;
 	border-radius: 0;
 	border: 0;
 }
@@ -227,7 +243,7 @@ input[type="range"]::-webkit-slider-thumb {
 	height: 19px;
 	width: 19px;
 	border-radius: 15px;
-	background: #03a9f4;
+	background: $primary-color;
 	cursor: pointer;
 	-webkit-appearance: none;
 	margin-top: -6.5px;
@@ -238,7 +254,7 @@ input[type="range"]::-moz-range-track {
 	height: 5.2px;
 	cursor: pointer;
 	box-shadow: 0;
-	background: #c2c0c2;
+	background: $light-grey-2;
 	border-radius: 0;
 	border: 0;
 }
@@ -249,7 +265,7 @@ input[type="range"]::-moz-range-thumb {
 	height: 19px;
 	width: 19px;
 	border-radius: 15px;
-	background: #03a9f4;
+	background: $primary-color;
 	cursor: pointer;
 	-webkit-appearance: none;
 	margin-top: -6.5px;
@@ -260,19 +276,19 @@ input[type="range"]::-ms-track {
 	height: 5.2px;
 	cursor: pointer;
 	box-shadow: 0;
-	background: #c2c0c2;
+	background: $light-grey-2;
 	border-radius: 1.3px;
 }
 
 input[type="range"]::-ms-fill-lower {
-	background: #c2c0c2;
+	background: $light-grey-2;
 	border: 0;
 	border-radius: 0;
 	box-shadow: 0;
 }
 
 input[type="range"]::-ms-fill-upper {
-	background: #c2c0c2;
+	background: $light-grey-2;
 	border: 0;
 	border-radius: 0;
 	box-shadow: 0;
@@ -284,7 +300,7 @@ input[type="range"]::-ms-thumb {
 	height: 15px;
 	width: 15px;
 	border-radius: 15px;
-	background: #03a9f4;
+	background: $primary-color;
 	cursor: pointer;
 	-webkit-appearance: none;
 	margin-top: 1.5px;
@@ -339,7 +355,7 @@ h5 {
 }
 
 .save-changes {
-	color: #fff;
+	color: $white;
 }
 
 .tag:not(:last-child) {

+ 57 - 105
frontend/components/Modals/EditSong.vue

@@ -186,29 +186,6 @@
 						v-model="editing.song.skipDuration"
 					/>
 				</p>
-				<article class="message" v-if="editing.type === 'songs'">
-					<div class="message-body">
-						<span class="reports-length">
-							{{ reports.length }}
-							<span
-								v-if="reports.length > 1 || reports.length <= 0"
-								>&nbsp;Reports</span
-							>
-							<span v-else>&nbsp;Report</span>
-						</span>
-						<div v-for="(report, index) in reports" :key="index">
-							<router-link
-								:to="{
-									path: '/admin/reports',
-									query: { id: report, returnToSong: true }
-								}"
-								class="report-link"
-							>
-								Report - {{ report }}
-							</router-link>
-						</div>
-					</div>
-				</article>
 				<hr />
 				<h5 class="has-text-centered">Spotify Information</h5>
 				<label class="label">Song title</label>
@@ -295,7 +272,6 @@ export default {
 	components: { Modal },
 	data() {
 		return {
-			reports: 0,
 			spotify: {
 				title: "",
 				artist: "",
@@ -310,7 +286,8 @@ export default {
 	computed: {
 		...mapState("admin/songs", {
 			video: state => state.video,
-			editing: state => state.editing
+			editing: state => state.editing,
+			songs: state => state.songs
 		}),
 		...mapState("modals", {
 			modals: state => state.modals.admin
@@ -318,8 +295,6 @@ export default {
 	},
 	methods: {
 		save(song, close) {
-			const _this = this;
-
 			if (!song.title)
 				return Toast.methods.addToast(
 					"Please fill in all fields",
@@ -419,13 +394,13 @@ export default {
 			}
 
 			return this.socket.emit(
-				`${_this.editing.type}.update`,
+				`${this.editing.type}.update`,
 				song._id,
 				song,
 				res => {
 					Toast.methods.addToast(res.message, 4000);
 					if (res.status === "success") {
-						_this.$parent.songs.forEach(originalSong => {
+						this.songs.forEach(originalSong => {
 							const updatedSong = song;
 							if (originalSong._id === updatedSong._id) {
 								Object.keys(originalSong).forEach(n => {
@@ -436,7 +411,7 @@ export default {
 						});
 					}
 					if (close)
-						_this.closeModal({
+						this.closeModal({
 							sector: "admin",
 							modal: "editSong"
 						});
@@ -444,35 +419,33 @@ export default {
 			);
 		},
 		settings(type) {
-			const _this = this;
 			switch (type) {
 				default:
 					break;
 				case "stop":
-					_this.stopVideo();
-					_this.pauseVideo(true);
+					this.stopVideo();
+					this.pauseVideo(true);
 					break;
 				case "pause":
-					_this.pauseVideo(true);
+					this.pauseVideo(true);
 					break;
 				case "play":
-					_this.pauseVideo(false);
+					this.pauseVideo(false);
 					break;
 				case "skipToLast10Secs":
-					_this.video.player.seekTo(
-						_this.editing.song.duration -
+					this.video.player.seekTo(
+						this.editing.song.duration -
 							10 +
-							_this.editing.song.skipDuration
+							this.editing.song.skipDuration
 					);
 					break;
 			}
 		},
 		changeVolume() {
-			const local = this;
 			const volume = document.getElementById("volumeSlider").value;
 			localStorage.setItem("volume", volume);
-			local.video.player.setVolume(volume);
-			if (volume > 0) local.video.player.unMute();
+			this.video.player.setVolume(volume);
+			if (volume > 0) this.video.player.unMute();
 		},
 		addTag(type) {
 			if (type === "genres") {
@@ -612,8 +585,6 @@ export default {
 		...mapActions("modals", ["closeModal"])
 	},
 	mounted() {
-		const _this = this;
-
 		// if (this.modals.editSong = false) this.video.player.stopVideo();
 
 		// this.loadVideoById(
@@ -624,42 +595,32 @@ export default {
 		this.initCanvas();
 
 		lofig.get("cookie.secure", res => {
-			_this.useHTTPS = res;
+			this.useHTTPS = res;
 		});
 
 		io.getSocket(socket => {
 			this.socket = socket;
-
-			if (this.editing.type === "songs") {
-				socket.emit(
-					"reports.getReportsForSong",
-					this.editing.song.songId,
-					res => {
-						this.reports = res.data;
-					}
-				);
-			}
 		});
 
 		setInterval(() => {
 			if (
-				_this.video.paused === false &&
-				_this.playerReady &&
-				_this.video.player.getCurrentTime() -
-					_this.editing.song.skipDuration >
-					_this.editing.song.duration
+				this.video.paused === false &&
+				this.playerReady &&
+				this.video.player.getCurrentTime() -
+					this.editing.song.skipDuration >
+					this.editing.song.duration
 			) {
-				_this.video.paused = false;
-				_this.video.player.stopVideo();
+				this.video.paused = false;
+				this.video.player.stopVideo();
 			}
 			if (this.playerReady) {
-				_this.getCurrentTime(3).then(time => {
+				this.getCurrentTime(3).then(time => {
 					this.youtubeVideoCurrentTime = time;
 					return time;
 				});
 			}
 
-			if (_this.video.paused === false) _this.drawCanvas();
+			if (this.video.paused === false) this.drawCanvas();
 		}, 200);
 
 		this.video.player = new window.YT.Player("player", {
@@ -673,44 +634,44 @@ export default {
 				showinfo: 0,
 				autoplay: 1
 			},
-			startSeconds: _this.editing.song.skipDuration,
+			startSeconds: this.editing.song.skipDuration,
 			events: {
 				onReady: () => {
 					let volume = parseInt(localStorage.getItem("volume"));
 					volume = typeof volume === "number" ? volume : 20;
-					console.log(`Seekto: ${_this.editing.song.skipDuration}`);
-					_this.video.player.seekTo(_this.editing.song.skipDuration);
-					_this.video.player.setVolume(volume);
-					if (volume > 0) _this.video.player.unMute();
-					this.youtubeVideoDuration = _this.video.player.getDuration();
+					console.log(`Seekto: ${this.editing.song.skipDuration}`);
+					this.video.player.seekTo(this.editing.song.skipDuration);
+					this.video.player.setVolume(volume);
+					if (volume > 0) this.video.player.unMute();
+					this.youtubeVideoDuration = this.video.player.getDuration();
 					this.youtubeVideoNote = "(~)";
-					_this.playerReady = true;
+					this.playerReady = true;
 
-					_this.drawCanvas();
+					this.drawCanvas();
 				},
 				onStateChange: event => {
 					if (event.data === 1) {
-						if (!_this.video.autoPlayed) {
-							_this.video.autoPlayed = true;
-							return _this.video.player.stopVideo();
+						if (!this.video.autoPlayed) {
+							this.video.autoPlayed = true;
+							return this.video.player.stopVideo();
 						}
 
-						_this.video.paused = false;
-						let youtubeDuration = _this.video.player.getDuration();
+						this.video.paused = false;
+						let youtubeDuration = this.video.player.getDuration();
 						this.youtubeVideoDuration = youtubeDuration;
 						this.youtubeVideoNote = "";
-						youtubeDuration -= _this.editing.song.skipDuration;
-						if (_this.editing.song.duration > youtubeDuration + 1) {
+						youtubeDuration -= this.editing.song.skipDuration;
+						if (this.editing.song.duration > youtubeDuration + 1) {
 							this.video.player.stopVideo();
-							_this.video.paused = true;
+							this.video.paused = true;
 							return Toast.methods.addToast(
 								"Video can't play. Specified duration is bigger than the YouTube song duration.",
 								4000
 							);
 						}
-						if (_this.editing.song.duration <= 0) {
+						if (this.editing.song.duration <= 0) {
 							this.video.player.stopVideo();
-							_this.video.paused = true;
+							this.video.paused = true;
 							return Toast.methods.addToast(
 								"Video can't play. Specified duration has to be more than 0 seconds.",
 								4000
@@ -718,11 +679,11 @@ export default {
 						}
 
 						if (
-							_this.video.player.getCurrentTime() <
-							_this.editing.song.skipDuration
+							this.video.player.getCurrentTime() <
+							this.editing.song.skipDuration
 						) {
-							return _this.video.player.seekTo(
-								_this.editing.song.skipDuration
+							return this.video.player.seekTo(
+								this.editing.song.skipDuration
 							);
 						}
 					} else if (event.data === 2) {
@@ -742,6 +703,8 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 input[type="range"] {
 	-webkit-appearance: none;
 	width: 100%;
@@ -757,7 +720,7 @@ input[type="range"]::-webkit-slider-runnable-track {
 	height: 5.2px;
 	cursor: pointer;
 	box-shadow: 0;
-	background: #c2c0c2;
+	background: $light-grey-2;
 	border-radius: 0;
 	border: 0;
 }
@@ -768,7 +731,7 @@ input[type="range"]::-webkit-slider-thumb {
 	height: 19px;
 	width: 19px;
 	border-radius: 15px;
-	background: #03a9f4;
+	background: $primary-color;
 	cursor: pointer;
 	-webkit-appearance: none;
 	margin-top: -6.5px;
@@ -779,7 +742,7 @@ input[type="range"]::-moz-range-track {
 	height: 5.2px;
 	cursor: pointer;
 	box-shadow: 0;
-	background: #c2c0c2;
+	background: $light-grey-2;
 	border-radius: 0;
 	border: 0;
 }
@@ -790,7 +753,7 @@ input[type="range"]::-moz-range-thumb {
 	height: 19px;
 	width: 19px;
 	border-radius: 15px;
-	background: #03a9f4;
+	background: $primary-color;
 	cursor: pointer;
 	-webkit-appearance: none;
 	margin-top: -6.5px;
@@ -801,19 +764,19 @@ input[type="range"]::-ms-track {
 	height: 5.2px;
 	cursor: pointer;
 	box-shadow: 0;
-	background: #c2c0c2;
+	background: $light-grey-2;
 	border-radius: 1.3px;
 }
 
 input[type="range"]::-ms-fill-lower {
-	background: #c2c0c2;
+	background: $light-grey-2;
 	border: 0;
 	border-radius: 0;
 	box-shadow: 0;
 }
 
 input[type="range"]::-ms-fill-upper {
-	background: #c2c0c2;
+	background: $light-grey-2;
 	border: 0;
 	border-radius: 0;
 	box-shadow: 0;
@@ -825,7 +788,7 @@ input[type="range"]::-ms-thumb {
 	height: 15px;
 	width: 15px;
 	border-radius: 15px;
-	background: #03a9f4;
+	background: $primary-color;
 	cursor: pointer;
 	-webkit-appearance: none;
 	margin-top: 1.5px;
@@ -880,21 +843,10 @@ h5 {
 }
 
 .save-changes {
-	color: #fff;
+	color: $white;
 }
 
 .tag:not(:last-child) {
 	margin-right: 5px;
 }
-
-.reports-length {
-	color: #ff4545;
-	font-weight: bold;
-	display: flex;
-	justify-content: center;
-}
-
-.report-link {
-	color: #000;
-}
 </style>

+ 26 - 27
frontend/components/Modals/EditStation.vue

@@ -105,9 +105,8 @@ export default {
 		editing: state => state.editing
 	}),
 	mounted() {
-		const _this = this;
 		io.getSocket(socket => {
-			_this.socket = socket;
+			this.socket = socket;
 			return socket;
 		});
 	},
@@ -197,8 +196,6 @@ export default {
 			);
 		},
 		updateDescription() {
-			const _this = this;
-
 			const { description } = this.editing;
 			if (!validation.isLength(description, 2, 200))
 				return Toast.methods.addToast(
@@ -223,14 +220,14 @@ export default {
 				description,
 				res => {
 					if (res.status === "success") {
-						if (_this.station) {
-							_this.station.description = description;
+						if (this.station) {
+							this.station.description = description;
 							return description;
 						}
 
-						_this.$parent.stations.forEach((station, index) => {
+						this.$parent.stations.forEach((station, index) => {
 							if (station._id === this.editing._id) {
-								_this.$parent.stations[
+								this.$parent.stations[
 									index
 								].description = description;
 								return description;
@@ -247,23 +244,23 @@ export default {
 			);
 		},
 		updatePrivacy() {
-			const _this = this;
 			return this.socket.emit(
 				"stations.updatePrivacy",
 				this.editing._id,
 				this.editing.privacy,
 				res => {
 					if (res.status === "success") {
-						if (_this.station) {
-							_this.station.privacy = _this.editing.privacy;
-							return _this.editing.privacy;
+						if (this.station) {
+							this.station.privacy = this.editing.privacy;
+							return this.editing.privacy;
 						}
 
-						_this.$parent.stations.forEach((station, index) => {
-							if (station._id === _this.editing._id) {
-								_this.$parent.stations[index].privacy =
-									_this.editing.privacy;
-								return _this.editing.privacy;
+						this.$parent.stations.forEach((station, index) => {
+							if (station._id === this.editing._id) {
+								this.$parent.stations[
+									index
+								].privacy = this.editing.privacy;
+								return this.editing.privacy;
 							}
 
 							return false;
@@ -277,23 +274,23 @@ export default {
 			);
 		},
 		updatePartyMode() {
-			const _this = this;
 			return this.socket.emit(
 				"stations.updatePartyMode",
 				this.editing._id,
 				this.editing.partyMode,
 				res => {
 					if (res.status === "success") {
-						if (_this.station) {
-							_this.station.partyMode = _this.editing.partyMode;
-							return _this.editing.partyMode;
+						if (this.station) {
+							this.station.partyMode = this.editing.partyMode;
+							return this.editing.partyMode;
 						}
 
-						_this.$parent.stations.forEach((station, index) => {
-							if (station._id === _this.editing._id) {
-								_this.$parent.stations[index].partyMode =
-									_this.editing.partyMode;
-								return _this.editing.partyMode;
+						this.$parent.stations.forEach((station, index) => {
+							if (station._id === this.editing._id) {
+								this.$parent.stations[
+									index
+								].partyMode = this.editing.partyMode;
+								return this.editing.partyMode;
 							}
 
 							return false;
@@ -317,6 +314,8 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .controls {
 	display: flex;
 
@@ -341,6 +340,6 @@ h5 {
 }
 
 .select:after {
-	border-color: #029ce3;
+	border-color: $primary-color;
 }
 </style>

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

@@ -109,6 +109,9 @@ export default {
 	computed: {
 		...mapState("admin/users", {
 			editing: state => state.editing
+		}),
+		...mapState({
+			userId: state => state.user.auth.userId
 		})
 	},
 	methods: {
@@ -166,7 +169,7 @@ export default {
 					if (
 						res.status === "success" &&
 						this.editing.role === "default" &&
-						this.editing._id === this.$parent.$parent.$parent.userId
+						this.editing._id === this.userId
 					)
 						window.location.reload();
 				}
@@ -203,9 +206,8 @@ export default {
 		...mapActions("modals", ["closeModal"])
 	},
 	mounted() {
-		const _this = this;
 		io.getSocket(socket => {
-			_this.socket = socket;
+			this.socket = socket;
 			return socket;
 		});
 	}
@@ -213,8 +215,10 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .save-changes {
-	color: #fff;
+	color: $white;
 }
 
 .tag:not(:last-child) {
@@ -222,6 +226,6 @@ export default {
 }
 
 .select:after {
-	border-color: #029ce3;
+	border-color: $primary-color;
 }
 </style>

+ 2 - 0
frontend/components/Modals/IssuesModal.vue

@@ -96,6 +96,8 @@ export default {
 </script>
 
 <style lang="scss">
+@import "styles/global.scss";
+
 .back-to-song {
 	display: flex;
 	margin-bottom: 20px;

+ 15 - 7
frontend/components/Modals/Login.vue

@@ -61,7 +61,7 @@
 				>
 				<a
 					class="button is-github"
-					:href="$parent.serverDomain + '/auth/github/authorize'"
+					:href="serverDomain + '/auth/github/authorize'"
 					@click="githubRedirect()"
 				>
 					<div class="icon">
@@ -86,7 +86,8 @@ export default {
 	data() {
 		return {
 			email: "",
-			password: ""
+			password: "",
+			serverDomain: ""
 		};
 	},
 	methods: {
@@ -109,21 +110,28 @@ export default {
 		},
 		...mapActions("modals", ["closeModal"]),
 		...mapActions("user/auth", ["login"])
+	},
+	mounted() {
+		lofig.get("serverDomain", res => {
+			this.serverDomain = res;
+		});
 	}
 };
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .button.is-github {
-	background-color: #333;
-	color: #fff !important;
+	background-color: $dark-grey-2;
+	color: $white !important;
 }
 
 .is-github:focus {
-	background-color: #1a1a1a;
+	background-color: $dark-grey-3;
 }
 .is-primary:focus {
-	background-color: #029ce3 !important;
+	background-color: $primary-color !important;
 }
 
 .invert {
@@ -131,6 +139,6 @@ export default {
 }
 
 a {
-	color: #029ce3;
+	color: $primary-color;
 }
 </style>

+ 4 - 3
frontend/components/Modals/MobileAlert.vue

@@ -30,9 +30,8 @@ export default {
 	},
 	methods: {
 		toggleModal() {
-			const _this = this;
-			_this.isModalActive = !_this.isModalActive;
-			if (_this.isModalActive) {
+			this.isModalActive = !this.isModalActive;
+			if (this.isModalActive) {
 				setTimeout(() => {
 					this.isModalActive = false;
 				}, 4000);
@@ -48,6 +47,8 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 @media (min-width: 735px) {
 	.modal {
 		display: none;

+ 5 - 6
frontend/components/Modals/Playlists/Create.vue

@@ -34,16 +34,13 @@ export default {
 		return {
 			playlist: {
 				displayName: null,
-				songs: [],
-				createdBy: this.$parent.$parent.username,
-				createdAt: Date.now()
+				songs: []
 			}
 		};
 	},
 	mounted() {
-		const _this = this;
 		io.getSocket(socket => {
-			_this.socket = socket;
+			this.socket = socket;
 		});
 	},
 	methods: {
@@ -83,6 +80,8 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .menu {
 	padding: 0 20px;
 }
@@ -93,7 +92,7 @@ export default {
 }
 
 .menu-list a:hover {
-	color: #000 !important;
+	color: $black !important;
 }
 
 li a {

+ 46 - 52
frontend/components/Modals/Playlists/Edit.vue

@@ -135,7 +135,7 @@
 </template>
 
 <script>
-import { mapState } from "vuex";
+import { mapState, mapActions } from "vuex";
 
 import { Toast } from "vue-roaster";
 import Modal from "../Modal.vue";
@@ -156,47 +156,46 @@ export default {
 		editing: state => state.editing
 	}),
 	mounted() {
-		const _this = this;
 		io.getSocket(socket => {
-			_this.socket = socket;
-			_this.socket.emit("playlists.getPlaylist", _this.editing, res => {
-				if (res.status === "success") _this.playlist = res.data;
-				_this.playlist.oldId = res.data._id;
+			this.socket = socket;
+			this.socket.emit("playlists.getPlaylist", this.editing, res => {
+				if (res.status === "success") this.playlist = res.data;
+				this.playlist.oldId = res.data._id;
 			});
-			_this.socket.on("event:playlist.addSong", data => {
-				if (_this.playlist._id === data.playlistId)
-					_this.playlist.songs.push(data.song);
+			this.socket.on("event:playlist.addSong", data => {
+				if (this.playlist._id === data.playlistId)
+					this.playlist.songs.push(data.song);
 			});
-			_this.socket.on("event:playlist.removeSong", data => {
-				if (_this.playlist._id === data.playlistId) {
-					_this.playlist.songs.forEach((song, index) => {
+			this.socket.on("event:playlist.removeSong", data => {
+				if (this.playlist._id === data.playlistId) {
+					this.playlist.songs.forEach((song, index) => {
 						if (song.songId === data.songId)
-							_this.playlist.songs.splice(index, 1);
+							this.playlist.songs.splice(index, 1);
 					});
 				}
 			});
-			_this.socket.on("event:playlist.updateDisplayName", data => {
-				if (_this.playlist._id === data.playlistId)
-					_this.playlist.displayName = data.displayName;
+			this.socket.on("event:playlist.updateDisplayName", data => {
+				if (this.playlist._id === data.playlistId)
+					this.playlist.displayName = data.displayName;
 			});
-			_this.socket.on("event:playlist.moveSongToBottom", data => {
-				if (_this.playlist._id === data.playlistId) {
+			this.socket.on("event:playlist.moveSongToBottom", data => {
+				if (this.playlist._id === data.playlistId) {
 					let songIndex;
-					_this.playlist.songs.forEach((song, index) => {
+					this.playlist.songs.forEach((song, index) => {
 						if (song.songId === data.songId) songIndex = index;
 					});
-					const song = _this.playlist.songs.splice(songIndex, 1)[0];
-					_this.playlist.songs.push(song);
+					const song = this.playlist.songs.splice(songIndex, 1)[0];
+					this.playlist.songs.push(song);
 				}
 			});
-			_this.socket.on("event:playlist.moveSongToTop", data => {
-				if (_this.playlist._id === data.playlistId) {
+			this.socket.on("event:playlist.moveSongToTop", data => {
+				if (this.playlist._id === data.playlistId) {
 					let songIndex;
-					_this.playlist.songs.forEach((song, index) => {
+					this.playlist.songs.forEach((song, index) => {
 						if (song.songId === data.songId) songIndex = index;
 					});
-					const song = _this.playlist.songs.splice(songIndex, 1)[0];
-					_this.playlist.songs.unshift(song);
+					const song = this.playlist.songs.splice(songIndex, 1)[0];
+					this.playlist.songs.unshift(song);
 				}
 			});
 		});
@@ -255,8 +254,7 @@ export default {
 			return this.formatTime(length);
 		},
 		searchForSongs() {
-			const _this = this;
-			let query = _this.songQuery;
+			let query = this.songQuery;
 			if (query.indexOf("&index=") !== -1) {
 				query = query.split("&index=");
 				query.pop();
@@ -267,11 +265,11 @@ export default {
 				query.pop();
 				query = query.join("");
 			}
-			_this.socket.emit("apis.searchYoutube", query, res => {
+			this.socket.emit("apis.searchYoutube", query, res => {
 				if (res.status === "success") {
-					_this.songQueryResults = [];
+					this.songQueryResults = [];
 					for (let i = 0; i < res.data.items.length; i += 1) {
-						_this.songQueryResults.push({
+						this.songQueryResults.push({
 							id: res.data.items[i].id.videoId,
 							url: `https://www.youtube.com/watch?v=${this.id}`,
 							title: res.data.items[i].snippet.title,
@@ -284,39 +282,36 @@ export default {
 			});
 		},
 		addSongToPlaylist(id) {
-			const _this = this;
-			_this.socket.emit(
+			this.socket.emit(
 				"playlists.addSongToPlaylist",
 				id,
-				_this.playlist._id,
+				this.playlist._id,
 				res => {
 					Toast.methods.addToast(res.message, 4000);
 				}
 			);
 		},
 		importPlaylist() {
-			const _this = this;
 			Toast.methods.addToast(
 				"Starting to import your playlist. This can take some time to do.",
 				4000
 			);
 			this.socket.emit(
 				"playlists.addSetToPlaylist",
-				_this.importQuery,
-				_this.playlist._id,
+				this.importQuery,
+				this.playlist._id,
 				res => {
 					if (res.status === "success")
-						_this.playlist.songs = res.data;
+						this.playlist.songs = res.data;
 					Toast.methods.addToast(res.message, 4000);
 				}
 			);
 		},
 		removeSongFromPlaylist(id) {
-			const _this = this;
 			this.socket.emit(
 				"playlists.removeSongFromPlaylist",
 				id,
-				_this.playlist._id,
+				this.playlist._id,
 				res => {
 					Toast.methods.addToast(res.message, 4000);
 				}
@@ -345,20 +340,17 @@ export default {
 			);
 		},
 		removePlaylist() {
-			const _this = this;
-			_this.socket.emit("playlists.remove", _this.playlist._id, res => {
+			this.socket.emit("playlists.remove", this.playlist._id, res => {
 				Toast.methods.addToast(res.message, 3000);
 				if (res.status === "success") {
-					_this.$parent.modals.editPlaylist = !_this.$parent.modals
-						.editPlaylist;
+					this.closeModal();
 				}
 			});
 		},
 		promoteSong(songId) {
-			const _this = this;
-			_this.socket.emit(
+			this.socket.emit(
 				"playlists.moveSongToTop",
-				_this.playlist._id,
+				this.playlist._id,
 				songId,
 				res => {
 					Toast.methods.addToast(res.message, 4000);
@@ -366,21 +358,23 @@ export default {
 			);
 		},
 		demoteSong(songId) {
-			const _this = this;
-			_this.socket.emit(
+			this.socket.emit(
 				"playlists.moveSongToBottom",
-				_this.playlist._id,
+				this.playlist._id,
 				songId,
 				res => {
 					Toast.methods.addToast(res.message, 4000);
 				}
 			);
-		}
+		},
+		...mapActions("modals", ["closeModal"])
 	}
 };
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .menu {
 	padding: 0 20px;
 }
@@ -391,7 +385,7 @@ export default {
 }
 
 .menu-list a:hover {
-	color: #000 !important;
+	color: $black !important;
 }
 
 li a {

+ 23 - 9
frontend/components/Modals/Register.vue

@@ -68,7 +68,7 @@
 				>
 				<a
 					class="button is-github"
-					:href="$parent.serverDomain + '/auth/github/authorize'"
+					:href="serverDomain + '/auth/github/authorize'"
 					@click="githubRedirect()"
 				>
 					<div class="icon">
@@ -95,13 +95,17 @@ export default {
 			recaptcha: {
 				key: "",
 				token: ""
-			}
+			},
+			serverDomain: ""
 		};
 	},
 	mounted() {
-		const _this = this;
+		lofig.get("serverDomain", res => {
+			this.serverDomain = res;
+		});
+
 		lofig.get("recaptcha", obj => {
-			_this.recaptcha.key = obj.key;
+			this.recaptcha.key = obj.key;
 
 			const recaptchaScript = document.createElement("script");
 			recaptchaScript.onload = () => {
@@ -109,7 +113,7 @@ export default {
 					grecaptcha
 						.execute(this.recaptcha.key, { action: "login" })
 						.then(token => {
-							_this.recaptcha.token = token;
+							this.recaptcha.token = token;
 						});
 				});
 			};
@@ -146,13 +150,15 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .button.is-github {
-	background-color: #333;
-	color: #fff !important;
+	background-color: $dark-grey-2;
+	color: $white !important;
 }
 
 .is-github:focus {
-	background-color: #1a1a1a;
+	background-color: $dark-grey-3;
 }
 .is-primary:focus {
 	background-color: #028bca !important;
@@ -167,6 +173,14 @@ export default {
 }
 
 a {
-	color: #029ce3;
+	color: $primary-color;
+}
+</style>
+
+<style lang="scss">
+@import "styles/global.scss";
+
+.grecaptcha-badge {
+	z-index: 2000;
 }
 </style>

+ 22 - 27
frontend/components/Modals/Report.vue

@@ -2,10 +2,7 @@
 	<modal title="Report">
 		<div slot="body">
 			<div class="columns song-types">
-				<div
-					v-if="$parent.previousSong !== null"
-					class="column song-type"
-				>
+				<div v-if="previousSong !== null" class="column song-type">
 					<div
 						class="card is-fullwidth"
 						:class="{ 'is-highlight-active': isPreviousSongActive }"
@@ -21,9 +18,7 @@
 								<figure class="media-left">
 									<p class="image is-64x64">
 										<img
-											:src="
-												$parent.previousSong.thumbnail
-											"
+											:src="previousSong.thumbnail"
 											onerror='this.src="/assets/notes-transparent.png"'
 										/>
 									</p>
@@ -32,13 +27,11 @@
 									<div class="content">
 										<p>
 											<strong>{{
-												$parent.previousSong.title
+												previousSong.title
 											}}</strong>
 											<br />
 											<small>{{
-												$parent.previousSong.artists.split(
-													" ,"
-												)
+												previousSong.artists.split(" ,")
 											}}</small>
 										</p>
 									</div>
@@ -52,7 +45,7 @@
 						/>
 					</div>
 				</div>
-				<div v-if="$parent.currentSong !== {}" class="column song-type">
+				<div v-if="currentSong !== {}" class="column song-type">
 					<div
 						class="card is-fullwidth"
 						:class="{ 'is-highlight-active': isCurrentSongActive }"
@@ -68,7 +61,7 @@
 								<figure class="media-left">
 									<p class="image is-64x64">
 										<img
-											:src="$parent.currentSong.thumbnail"
+											:src="currentSong.thumbnail"
 											onerror='this.src="/assets/notes-transparent.png"'
 										/>
 									</p>
@@ -77,13 +70,11 @@
 									<div class="content">
 										<p>
 											<strong>{{
-												$parent.currentSong.title
+												currentSong.title
 											}}</strong>
 											<br />
 											<small>{{
-												$parent.currentSong.artists.split(
-													" ,"
-												)
+												currentSong.artists.split(" ,")
 											}}</small>
 										</p>
 									</div>
@@ -158,7 +149,7 @@
 </template>
 
 <script>
-import { mapActions } from "vuex";
+import { mapState, mapActions } from "vuex";
 
 import { Toast } from "vue-roaster";
 import Modal from "./Modal.vue";
@@ -173,7 +164,7 @@ export default {
 			isCurrentSongActive: true,
 			report: {
 				resolved: false,
-				songId: this.$parent.currentSong.songId,
+				songId: this.currentSong.songId,
 				description: "",
 				issues: [
 					{ name: "Video", reasons: [] },
@@ -216,20 +207,22 @@ export default {
 			]
 		};
 	},
+	computed: mapState({
+		currentSong: state => state.station.currentSong,
+		previousSong: state => state.station.previousSong
+	}),
 	mounted() {
-		const _this = this;
 		io.getSocket(socket => {
-			_this.socket = socket;
+			this.socket = socket;
 		});
 	},
 	methods: {
 		create() {
-			const _this = this;
 			console.log(this.report);
-			_this.socket.emit("reports.create", _this.report, res => {
+			this.socket.emit("reports.create", this.report, res => {
 				Toast.methods.addToast(res.message, 4000);
 				if (res.status === "success")
-					_this.closeModal({
+					this.closeModal({
 						sector: "station",
 						modal: "report"
 					});
@@ -241,11 +234,11 @@ export default {
 		},
 		highlight(type) {
 			if (type === "currentSong") {
-				this.report.songId = this.$parent.currentSong.songId;
+				this.report.songId = this.currentSong.songId;
 				this.isPreviousSongActive = false;
 				this.isCurrentSongActive = true;
 			} else if (type === "previousSong") {
-				this.report.songId = this.$parent.previousSong.songId;
+				this.report.songId = this.previousSong.songId;
 				this.isCurrentSongActive = false;
 				this.isPreviousSongActive = true;
 			}
@@ -268,6 +261,8 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 h6 {
 	margin-bottom: 15px;
 }
@@ -305,6 +300,6 @@ h6 {
 }
 
 .is-highlight-active {
-	border: 3px #03a9f4 solid;
+	border: 3px $primary-color solid;
 }
 </style>

+ 1 - 2
frontend/components/Modals/ViewPunishment.vue

@@ -75,9 +75,8 @@ export default {
 		})
 	},
 	mounted() {
-		const _this = this;
 		io.getSocket(socket => {
-			_this.socket = socket;
+			this.socket = socket;
 			return socket;
 		});
 	},

+ 7 - 6
frontend/components/Modals/WhatIsNew.vue

@@ -82,12 +82,11 @@ export default {
 		};
 	},
 	mounted() {
-		const _this = this;
 		io.getSocket(true, socket => {
-			_this.socket = socket;
-			_this.socket.emit("news.newest", res => {
-				_this.news = res.data;
-				if (_this.news && localStorage.getItem("firstVisited")) {
+			this.socket = socket;
+			this.socket.emit("news.newest", res => {
+				this.news = res.data;
+				if (this.news && localStorage.getItem("firstVisited")) {
 					if (localStorage.getItem("whatIsNew")) {
 						if (
 							parseInt(localStorage.getItem("whatIsNew")) <
@@ -130,6 +129,8 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .modal-card-head {
 	border-bottom: none;
 	background-color: ghostwhite;
@@ -158,7 +159,7 @@ export default {
 		padding: 12px;
 		text-transform: uppercase;
 		font-weight: bold;
-		color: #fff;
+		color: $white;
 	}
 
 	.sect-head-features {

+ 37 - 32
frontend/components/Sidebars/Playlist.vue

@@ -14,7 +14,7 @@
 							<a
 								v-if="
 									isNotSelected(playlist._id) &&
-										!$parent.station.partyMode
+										!station.partyMode
 								"
 								href="#"
 								@click="selectPlaylist(playlist._id)"
@@ -46,7 +46,7 @@
 </template>
 
 <script>
-import { mapActions } from "vuex";
+import { mapState, mapActions } from "vuex";
 
 import { Toast } from "vue-roaster";
 import io from "../../io";
@@ -57,6 +57,14 @@ export default {
 			playlists: []
 		};
 	},
+	computed: {
+		...mapState("modals", {
+			modals: state => state.modals.station
+		}),
+		...mapState({
+			station: state => state.station.station
+		})
+	},
 	methods: {
 		edit(id) {
 			this.editPlaylist(id);
@@ -65,7 +73,7 @@ export default {
 		selectPlaylist(id) {
 			this.socket.emit(
 				"stations.selectPrivatePlaylist",
-				this.$parent.station._id,
+				this.station._id,
 				id,
 				res => {
 					if (res.status === "failure")
@@ -75,12 +83,8 @@ export default {
 			);
 		},
 		isNotSelected(id) {
-			const _this = this;
 			// TODO Also change this once it changes for a station
-			if (
-				_this.$parent.station &&
-				_this.$parent.station.privatePlaylist === id
-			)
+			if (this.station && this.station.privatePlaylist === id)
 				return false;
 			return true;
 		},
@@ -89,44 +93,43 @@ export default {
 	},
 	mounted() {
 		// TODO: Update when playlist is removed/created
-		const _this = this;
 		io.getSocket(socket => {
-			_this.socket = socket;
-			_this.socket.emit("playlists.indexForUser", res => {
-				if (res.status === "success") _this.playlists = res.data;
+			this.socket = socket;
+			this.socket.emit("playlists.indexForUser", res => {
+				if (res.status === "success") this.playlists = res.data;
 			});
-			_this.socket.on("event:playlist.create", playlist => {
-				_this.playlists.push(playlist);
+			this.socket.on("event:playlist.create", playlist => {
+				this.playlists.push(playlist);
 			});
-			_this.socket.on("event:playlist.delete", playlistId => {
-				_this.playlists.forEach((playlist, index) => {
+			this.socket.on("event:playlist.delete", playlistId => {
+				this.playlists.forEach((playlist, index) => {
 					if (playlist._id === playlistId) {
-						_this.playlists.splice(index, 1);
+						this.playlists.splice(index, 1);
 					}
 				});
 			});
-			_this.socket.on("event:playlist.addSong", data => {
-				_this.playlists.forEach((playlist, index) => {
+			this.socket.on("event:playlist.addSong", data => {
+				this.playlists.forEach((playlist, index) => {
 					if (playlist._id === data.playlistId) {
-						_this.playlists[index].songs.push(data.song);
+						this.playlists[index].songs.push(data.song);
 					}
 				});
 			});
-			_this.socket.on("event:playlist.removeSong", data => {
-				_this.playlists.forEach((playlist, index) => {
+			this.socket.on("event:playlist.removeSong", data => {
+				this.playlists.forEach((playlist, index) => {
 					if (playlist._id === data.playlistId) {
-						_this.playlists[index].songs.forEach((song, index2) => {
+						this.playlists[index].songs.forEach((song, index2) => {
 							if (song._id === data.songId) {
-								_this.playlists[index].songs.splice(index2, 1);
+								this.playlists[index].songs.splice(index2, 1);
 							}
 						});
 					}
 				});
 			});
-			_this.socket.on("event:playlist.updateDisplayName", data => {
-				_this.playlists.forEach((playlist, index) => {
+			this.socket.on("event:playlist.updateDisplayName", data => {
+				this.playlists.forEach((playlist, index) => {
 					if (playlist._id === data.playlistId) {
-						_this.playlists[index].displayName = data.displayName;
+						this.playlists[index].displayName = data.displayName;
 					}
 				});
 			});
@@ -136,6 +139,8 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .sidebar {
 	position: fixed;
 	z-index: 1;
@@ -143,7 +148,7 @@ export default {
 	right: 0;
 	width: 300px;
 	height: 100vh;
-	background-color: #fff;
+	background-color: $white;
 	box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16),
 		0 2px 10px 0 rgba(0, 0, 0, 0.12);
 }
@@ -158,7 +163,7 @@ export default {
 }
 
 .inner-wrapper {
-	top: 64px;
+	top: 60px;
 	position: relative;
 }
 
@@ -176,7 +181,7 @@ export default {
 	background-color: rgb(3, 169, 244);
 	text-align: center;
 	padding: 10px;
-	color: white;
+	color: $white;
 	font-weight: 600;
 }
 
@@ -186,7 +191,7 @@ export default {
 	height: 40px;
 	border-radius: 0;
 	background: rgba(3, 169, 244, 1);
-	color: #fff !important;
+	color: $white !important;
 	border: 0;
 
 	&:active,
@@ -196,7 +201,7 @@ export default {
 }
 
 .create-playlist:focus {
-	background: #029ce3;
+	background: $primary-color;
 }
 
 .none-found {

+ 41 - 40
frontend/components/Sidebars/SongsList.vue

@@ -1,18 +1,18 @@
 <template>
 	<div class="sidebar" transition="slide">
 		<div class="inner-wrapper">
-			<div v-if="$parent.type === 'community'" class="title">
+			<div v-if="station.type === 'community'" class="title">
 				Queue
 			</div>
 			<div v-else class="title">
 				Playlist
 			</div>
 
-			<article v-if="!$parent.noSong" class="media">
-				<figure v-if="$parent.currentSong.thumbnail" class="media-left">
+			<article v-if="!noSong" class="media">
+				<figure v-if="currentSong.thumbnail" class="media-left">
 					<p class="image is-64x64">
 						<img
-							:src="$parent.currentSong.thumbnail"
+							:src="currentSong.thumbnail"
 							onerror="this.src='/assets/notes-transparent.png'"
 						/>
 					</p>
@@ -21,22 +21,22 @@
 					<div class="content">
 						<p>
 							Current Song:
-							<strong>{{ $parent.currentSong.title }}</strong>
+							<strong>{{ currentSong.title }}</strong>
 							<br />
-							<small>{{ $parent.currentSong.artists }}</small>
+							<small>{{ currentSong.artists }}</small>
 						</p>
 					</div>
 				</div>
 				<div class="media-right">
-					{{ $parent.formatTime($parent.currentSong.duration) }}
+					{{ $parent.formatTime(currentSong.duration) }}
 				</div>
 			</article>
-			<p v-if="$parent.noSong" class="center">
+			<p v-if="noSong" class="center">
 				There is currently no song playing.
 			</p>
 
 			<article
-				v-for="(song, index) in $parent.songsList"
+				v-for="(song, index) in songsList"
 				:key="index"
 				class="media"
 			>
@@ -49,8 +49,8 @@
 						<small>{{ song.artists.join(", ") }}</small>
 						<div
 							v-if="
-								$parent.type === 'community' &&
-									$parent.station.partyMode === true
+								station.type === 'community' &&
+									station.partyMode === true
 							"
 						>
 							<small>
@@ -78,16 +78,16 @@
 			</article>
 			<div
 				v-if="
-					$parent.type === 'community' &&
-						$parent.$parent.loggedIn &&
-						$parent.station.partyMode === true
+					station.type === 'community' &&
+						loggedIn &&
+						station.partyMode === true
 				"
 			>
 				<button
 					v-if="
-						($parent.station.locked && isOwnerOnly()) ||
-							!$parent.station.locked ||
-							($parent.station.locked &&
+						(station.locked && isOwnerOnly()) ||
+							!station.locked ||
+							(station.locked &&
 								isAdminOnly() &&
 								dismissedWarning)
 					"
@@ -103,7 +103,7 @@
 				</button>
 				<button
 					v-if="
-						$parent.station.locked &&
+						station.locked &&
 							isAdminOnly() &&
 							!isOwnerOnly() &&
 							!dismissedWarning
@@ -114,11 +114,7 @@
 					THIS STATION'S QUEUE IS LOCKED.
 				</button>
 				<button
-					v-if="
-						$parent.station.locked &&
-							!isAdminOnly() &&
-							!isOwnerOnly()
-					"
+					v-if="station.locked && !isAdminOnly() && !isOwnerOnly()"
 					class="button add-to-queue add-to-queue-disabled"
 				>
 					THIS STATION'S QUEUE IS LOCKED.
@@ -129,7 +125,7 @@
 </template>
 
 <script>
-import { mapActions } from "vuex";
+import { mapState, mapActions } from "vuex";
 
 import { Toast } from "vue-roaster";
 
@@ -141,23 +137,26 @@ export default {
 			dismissedWarning: false
 		};
 	},
+	computed: mapState({
+		loggedIn: state => state.user.auth.loggedIn,
+		userId: state => state.user.auth.userId,
+		role: state => state.user.auth.role,
+		station: state => state.station.station,
+		currentSong: state => state.station.currentSong,
+		songsList: state => state.station.songsList,
+		noSong: state => state.station.noSong
+	}),
 	methods: {
 		isOwnerOnly() {
-			return (
-				this.$parent.$parent.loggedIn &&
-				this.$parent.$parent.userId === this.$parent.station.owner
-			);
+			return this.loggedIn && this.userId === this.station.owner;
 		},
 		isAdminOnly() {
-			return (
-				this.$parent.$parent.loggedIn &&
-				this.$parent.$parent.role === "admin"
-			);
+			return this.loggedIn && this.role === "admin";
 		},
 		removeFromQueue(songId) {
 			window.socket.emit(
 				"stations.removeFromQueue",
-				this.$parent.station._id,
+				this.station._id,
 				songId,
 				res => {
 					if (res.status === "success") {
@@ -172,9 +171,9 @@ export default {
 		...mapActions("modals", ["openModal"])
 	},
 	mounted() {
-		/* let _this = this;
+		/*
 			io.getSocket((socket) => {
-				_this.socket = socket;
+				this.socket = socket;
 
 			}); */
 	},
@@ -183,6 +182,8 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .sidebar {
 	position: fixed;
 	z-index: 1;
@@ -190,13 +191,13 @@ export default {
 	right: 0;
 	width: 300px;
 	height: 100vh;
-	background-color: #fff;
+	background-color: $white;
 	box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16),
 		0 2px 10px 0 rgba(0, 0, 0, 0.12);
 }
 
 .inner-wrapper {
-	top: 64px;
+	top: 60px;
 	position: relative;
 	overflow: auto;
 	height: 100%;
@@ -216,7 +217,7 @@ export default {
 	background-color: rgb(3, 169, 244);
 	text-align: center;
 	padding: 10px;
-	color: white;
+	color: $white;
 	font-weight: 600;
 }
 
@@ -244,7 +245,7 @@ export default {
 	height: 40px;
 	border-radius: 0;
 	background: rgb(3, 169, 244);
-	color: #fff !important;
+	color: $white !important;
 	border: 0;
 	&:active,
 	&:focus {
@@ -264,7 +265,7 @@ export default {
 }
 
 .add-to-queue:focus {
-	background: #029ce3;
+	background: $primary-color;
 }
 
 .media-right {

+ 18 - 5
frontend/components/Sidebars/UsersList.vue

@@ -4,10 +4,10 @@
 			<div class="title">
 				Users
 			</div>
-			<h5 class="center">Total users: {{ $parent.userCount }}</h5>
+			<h5 class="center">Total users: {{ userCount }}</h5>
 			<aside class="menu">
 				<ul class="menu-list">
-					<li v-for="(username, index) in $parent.users" :key="index">
+					<li v-for="(username, index) in users" :key="index">
 						<router-link
 							:to="{ name: 'profile', params: { username } }"
 							target="_blank"
@@ -21,7 +21,20 @@
 	</div>
 </template>
 
+<script>
+import { mapState } from "vuex";
+
+export default {
+	computed: mapState({
+		users: state => state.station.users,
+		userCount: state => state.station.userCount
+	})
+};
+</script>
+
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .sidebar {
 	position: fixed;
 	z-index: 1;
@@ -29,13 +42,13 @@
 	right: 0;
 	width: 300px;
 	height: 100vh;
-	background-color: #fff;
+	background-color: $white;
 	box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16),
 		0 2px 10px 0 rgba(0, 0, 0, 0.12);
 }
 
 .inner-wrapper {
-	top: 64px;
+	top: 60px;
 	position: relative;
 }
 
@@ -53,7 +66,7 @@
 	background-color: rgb(3, 169, 244);
 	text-align: center;
 	padding: 10px;
-	color: white;
+	color: $white;
 	font-weight: 600;
 }
 

+ 48 - 44
frontend/components/Station/CommunityHeader.vue

@@ -15,7 +15,7 @@
 			</div>
 
 			<div class="nav-center stationDisplayName">
-				{{ $parent.station.displayName }}
+				{{ station.displayName }}
 			</div>
 
 			<span class="nav-toggle" v-on:click="controlBar = !controlBar">
@@ -26,26 +26,23 @@
 
 			<div class="nav-right nav-menu" :class="{ 'is-active': isMobile }">
 				<router-link
-					v-if="$parent.$parent.role === 'admin'"
+					v-if="role === 'admin'"
 					class="nav-item is-tab admin"
 					href="#"
 					:to="{ path: '/admin' }"
 				>
 					<strong>Admin</strong>
 				</router-link>
-				<span v-if="$parent.$parent.loggedIn" class="grouped">
+				<span v-if="loggedIn" class="grouped">
 					<router-link
 						class="nav-item is-tab"
-						:to="{ path: '/u/' + $parent.$parent.username }"
+						:to="{ path: '/u/' + username }"
 						>Profile</router-link
 					>
 					<router-link class="nav-item is-tab" to="/settings"
 						>Settings</router-link
 					>
-					<a
-						class="nav-item is-tab"
-						href="#"
-						@click="$parent.$parent.logout()"
+					<a class="nav-item is-tab" href="#" @click="logout()"
 						>Logout</a
 					>
 				</span>
@@ -93,7 +90,7 @@
 						<span class="icon-purpose">Skip current song</span>
 					</a>
 					<a
-						v-if="isOwner() && $parent.paused"
+						v-if="isOwner() && paused"
 						class="sidebar-item"
 						href="#"
 						@click="$parent.resumeStation()"
@@ -104,7 +101,7 @@
 						<span class="icon-purpose">Resume station</span>
 					</a>
 					<a
-						v-if="isOwner() && !$parent.paused"
+						v-if="isOwner() && !paused"
 						class="sidebar-item"
 						href="#"
 						@click="$parent.pauseStation()"
@@ -116,13 +113,9 @@
 					</a>
 					<hr />
 				</div>
-				<div v-if="$parent.$parent.loggedIn && !$parent.noSong">
+				<div v-if="loggedIn && !noSong">
 					<a
-						v-if="
-							!isOwner() &&
-								$parent.$parent.loggedIn &&
-								!$parent.noSong
-						"
+						v-if="!isOwner() && loggedIn && !noSong"
 						class="sidebar-item"
 						href="#"
 						@click="$parent.voteSkipStation()"
@@ -131,12 +124,12 @@
 							<i class="material-icons">skip_next</i>
 						</span>
 						<span class="skip-votes">{{
-							$parent.currentSong.skipVotes
+							currentSong.skipVotes
 						}}</span>
 						<span class="icon-purpose">Skip current song</span>
 					</a>
 					<a
-						v-if="$parent.$parent.loggedIn && !$parent.noSong"
+						v-if="loggedIn && !noSong"
 						class="sidebar-item"
 						href="#"
 						@click="
@@ -156,7 +149,7 @@
 					<hr />
 				</div>
 				<a
-					v-if="$parent.station.partyMode === true"
+					v-if="station.partyMode === true"
 					class="sidebar-item"
 					href="#"
 					@click="$parent.toggleSidebar('songslist')"
@@ -179,7 +172,7 @@
 					>
 				</a>
 				<a
-					v-if="$parent.$parent.loggedIn"
+					v-if="loggedIn"
 					class="sidebar-item"
 					href="#"
 					@click="$parent.toggleSidebar('playlist')"
@@ -195,7 +188,7 @@
 </template>
 
 <script>
-import { mapActions } from "vuex";
+import { mapState, mapActions } from "vuex";
 
 export default {
 	data() {
@@ -210,6 +203,15 @@ export default {
 			}
 		};
 	},
+	computed: mapState({
+		loggedIn: state => state.user.auth.loggedIn,
+		userId: state => state.user.auth.userId,
+		username: state => state.user.auth.username,
+		role: state => state.user.auth.role,
+		station: state => state.station.station,
+		paused: state => state.station.paused,
+		noSong: state => state.station.noSong
+	}),
 	mounted() {
 		lofig.get("frontendDomain", res => {
 			this.frontendDomain = res;
@@ -223,20 +225,19 @@ export default {
 	methods: {
 		isOwner() {
 			return (
-				this.$parent.$parent.loggedIn &&
-				(this.$parent.$parent.role === "admin" ||
-					this.$parent.$parent.userId === this.$parent.station.owner)
+				this.loggedIn &&
+				(this.role === "admin" || this.userId === this.station.owner)
 			);
 		},
 		settings() {
 			this.editStation({
-				_id: this.$parent.station._id,
-				name: this.$parent.station.name,
-				type: this.$parent.type,
-				partyMode: this.$parent.station.partyMode,
-				description: this.$parent.station.description,
-				privacy: this.$parent.station.privacy,
-				displayName: this.$parent.station.displayName
+				_id: this.station._id,
+				name: this.station.name,
+				type: this.station.type,
+				partyMode: this.station.partyMode,
+				description: this.station.description,
+				privacy: this.station.privacy,
+				displayName: this.station.displayName
 			});
 			this.openModal({
 				sector: "station",
@@ -244,14 +245,17 @@ export default {
 			});
 		},
 		...mapActions("modals", ["openModal"]),
-		...mapActions("station", ["editStation"])
+		...mapActions("station", ["editStation"]),
+		...mapActions("user/auth", ["logout"])
 	}
 };
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .nav {
-	background-color: #03a9f4;
+	background-color: $primary-color;
 	line-height: 64px;
 	border-radius: 0% 0% 33% 33% / 0% 0% 7% 7%;
 
@@ -259,7 +263,7 @@ export default {
 		font-size: 2.1rem !important;
 		line-height: 64px !important;
 		padding: 0 20px;
-		color: #ffffff;
+		color: $white;
 		font-family: Pacifico, cursive;
 		filter: brightness(0) invert(1);
 
@@ -270,11 +274,11 @@ export default {
 }
 
 a.nav-item {
-	color: #ffffff;
+	color: $white;
 	font-size: 17px;
 
 	&:hover {
-		color: #ffffff;
+		color: $white;
 	}
 
 	padding: 0 12px;
@@ -291,11 +295,11 @@ a.nav-item {
 
 a.nav-item.is-tab:hover {
 	border-bottom: none;
-	border-top: solid 1px #ffffff;
+	border-top: solid 1px $white;
 }
 
 .admin strong {
-	color: #9d42b1;
+	color: $purple;
 }
 
 .grouped {
@@ -315,7 +319,7 @@ a.nav-item.is-tab:hover {
 
 @media screen and (max-width: 998px) {
 	.nav-menu {
-		background-color: white;
+		background-color: $white;
 		box-shadow: 0 4px 7px rgba(10, 10, 10, 0.1);
 		left: 0;
 		display: none;
@@ -338,7 +342,7 @@ a.nav-item.is-tab:hover {
 .nav-center {
 	display: flex;
 	align-items: center;
-	color: #03a9f4;
+	color: $primary-color;
 	font-size: 22px;
 	position: absolute;
 	margin: auto;
@@ -348,7 +352,7 @@ a.nav-item.is-tab:hover {
 }
 
 .nav-right.is-active .nav-item {
-	background: #03a9f4;
+	background: $primary-color;
 	border: 0;
 }
 
@@ -359,7 +363,7 @@ a.nav-item.is-tab:hover {
 	left: 0;
 	width: 64px;
 	height: 100vh;
-	background-color: #03a9f4;
+	background-color: $primary-color;
 	box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16),
 		0 2px 10px 0 rgba(0, 0, 0, 0.12);
 
@@ -403,7 +407,7 @@ a.nav-item.is-tab:hover {
 .control-sidebar .sidebar-item {
 	font-size: 2rem;
 	height: 50px;
-	color: white;
+	color: $white;
 	-webkit-box-align: center;
 	-ms-flex-align: center;
 	align-items: center;
@@ -430,7 +434,7 @@ a.nav-item.is-tab:hover {
 	width: 160px;
 	font-size: 12px;
 	background-color: rgba(3, 169, 244, 0.8);
-	color: #fff;
+	color: $white;
 	text-align: center;
 	border-radius: 6px;
 	padding: 5px;

+ 47 - 51
frontend/components/Station/OfficialHeader.vue

@@ -11,7 +11,7 @@
 			</div>
 
 			<div class="nav-center stationDisplayName">
-				{{ $parent.station.displayName }}
+				{{ station.displayName }}
 			</div>
 
 			<span class="nav-toggle" v-on:click="controlBar = !controlBar">
@@ -22,26 +22,24 @@
 
 			<div class="nav-right nav-menu" :class="{ 'is-active': isMobile }">
 				<router-link
-					v-if="$parent.$parent.role === 'admin'"
+					v-if="role === 'admin'"
 					class="nav-item is-tab admin"
 					href="#"
 					:to="{ path: '/admin' }"
 				>
 					<strong>Admin</strong>
 				</router-link>
-				<span v-if="$parent.$parent.loggedIn" class="grouped">
+				<span v-if="loggedIn" class="grouped">
 					<router-link
 						class="nav-item is-tab"
 						href="#"
-						:to="{ path: '/u/' + $parent.$parent.username }"
+						:to="{ path: '/u/' + username }"
 						>Profile</router-link
 					>
 					<router-link class="nav-item is-tab" to="/settings"
 						>Settings</router-link
 					>
-					<a class="nav-item is-tab" @click="$parent.$parent.logout()"
-						>Logout</a
-					>
+					<a class="nav-item is-tab" @click="logout()">Logout</a>
 				</span>
 				<span v-else class="grouped">
 					<a
@@ -87,7 +85,7 @@
 						<span class="icon-purpose">Skip current song</span>
 					</a>
 					<a
-						v-if="isOwner() && !$parent.paused"
+						v-if="isOwner() && !paused"
 						class="sidebar-item"
 						href="#"
 						@click="$parent.pauseStation()"
@@ -98,7 +96,7 @@
 						<span class="icon-purpose">Pause station</span>
 					</a>
 					<a
-						v-if="isOwner() && $parent.paused"
+						v-if="isOwner() && paused"
 						class="sidebar-item"
 						href="#"
 						@click="$parent.resumeStation()"
@@ -110,12 +108,9 @@
 					</a>
 					<hr />
 				</div>
-				<div v-if="$parent.$parent.loggedIn">
+				<div v-if="loggedIn">
 					<a
-						v-if="
-							$parent.type === 'official' &&
-								$parent.$parent.loggedIn
-						"
+						v-if="station.type === 'official' && loggedIn"
 						class="sidebar-item"
 						href="#"
 						@click="
@@ -131,11 +126,7 @@
 						<span class="icon-purpose">Add song to queue</span>
 					</a>
 					<a
-						v-if="
-							!isOwner() &&
-								$parent.$parent.loggedIn &&
-								!$parent.noSong
-						"
+						v-if="!isOwner() && loggedIn && !noSong"
 						class="sidebar-item"
 						href="#"
 						@click="$parent.voteSkipStation()"
@@ -144,16 +135,12 @@
 							<i class="material-icons">skip_next</i>
 						</span>
 						<span class="skip-votes">{{
-							$parent.currentSong.skipVotes
+							currentSong.skipVotes
 						}}</span>
 						<span class="icon-purpose">Skip current song</span>
 					</a>
 					<a
-						v-if="
-							$parent.$parent.loggedIn &&
-								!$parent.noSong &&
-								!$parent.simpleSong
-						"
+						v-if="loggedIn && !noSong && !currentSong.simpleSong"
 						class="sidebar-item"
 						href="#"
 						@click="
@@ -169,7 +156,7 @@
 						<span class="icon-purpose">Report a song</span>
 					</a>
 					<a
-						v-if="$parent.$parent.loggedIn && !$parent.noSong"
+						v-if="loggedIn && !noSong"
 						class="sidebar-item"
 						href="#"
 						@click="
@@ -216,7 +203,7 @@
 </template>
 
 <script>
-import { mapActions } from "vuex";
+import { mapState, mapActions } from "vuex";
 
 export default {
 	data() {
@@ -231,6 +218,15 @@ export default {
 			}
 		};
 	},
+	computed: mapState({
+		role: state => state.user.auth.role,
+		username: state => state.user.auth.username,
+		loggedIn: state => state.user.auth.loggedIn,
+		station: state => state.station.station,
+		currentSong: state => state.station.currentSong,
+		paused: state => state.station.paused,
+		noSong: state => state.station.noSong
+	}),
 	mounted() {
 		lofig.get("frontendDomain", res => {
 			this.frontendDomain = res;
@@ -243,20 +239,17 @@ export default {
 	},
 	methods: {
 		isOwner() {
-			return (
-				this.$parent.$parent.loggedIn &&
-				this.$parent.$parent.role === "admin"
-			);
+			return this.loggedIn && this.role === "admin";
 		},
 		settings() {
 			this.editStation({
-				_id: this.$parent.station._id,
-				name: this.$parent.station.name,
-				type: this.$parent.type,
-				partyMode: this.$parent.station.partyMode,
-				description: this.$parent.station.description,
-				privacy: this.$parent.station.privacy,
-				displayName: this.$parent.station.displayName
+				_id: this.station._id,
+				name: this.station.name,
+				type: this.station.type,
+				partyMode: this.station.partyMode,
+				description: this.station.description,
+				privacy: this.station.privacy,
+				displayName: this.station.displayName
 			});
 			this.openModal({
 				sector: "station",
@@ -264,14 +257,17 @@ export default {
 			});
 		},
 		...mapActions("modals", ["openModal"]),
-		...mapActions("station", ["editStation"])
+		...mapActions("station", ["editStation"]),
+		...mapActions("user/auth", ["logout"])
 	}
 };
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .nav {
-	background-color: #03a9f4;
+	background-color: $primary-color;
 	line-height: 64px;
 	border-radius: 0% 0% 33% 33% / 0% 0% 7% 7%;
 
@@ -279,7 +275,7 @@ export default {
 		font-size: 2.1rem !important;
 		line-height: 64px !important;
 		padding: 0 20px;
-		color: #ffffff;
+		color: $white;
 		font-family: Pacifico, cursive;
 		filter: brightness(0) invert(1);
 
@@ -290,11 +286,11 @@ export default {
 }
 
 a.nav-item {
-	color: #ffffff;
+	color: $white;
 	font-size: 17px;
 
 	&:hover {
-		color: #ffffff;
+		color: $white;
 	}
 
 	padding: 0 12px;
@@ -311,11 +307,11 @@ a.nav-item {
 
 a.nav-item.is-tab:hover {
 	border-bottom: none;
-	border-top: solid 1px #ffffff;
+	border-top: solid 1px $white;
 }
 
 .admin strong {
-	color: #9d42b1;
+	color: $purple;
 }
 
 .grouped {
@@ -335,7 +331,7 @@ a.nav-item.is-tab:hover {
 
 @media screen and (max-width: 998px) {
 	.nav-menu {
-		background-color: white;
+		background-color: $white;
 		box-shadow: 0 4px 7px rgba(10, 10, 10, 0.1);
 		left: 0;
 		display: none;
@@ -358,7 +354,7 @@ a.nav-item.is-tab:hover {
 .nav-center {
 	display: flex;
 	align-items: center;
-	color: #03a9f4;
+	color: $primary-color;
 	font-size: 22px;
 	position: absolute;
 	margin: auto;
@@ -368,7 +364,7 @@ a.nav-item.is-tab:hover {
 }
 
 .nav-right.is-active .nav-item {
-	background: #03a9f4;
+	background: $primary-color;
 	border: 0;
 }
 
@@ -383,7 +379,7 @@ a.nav-item.is-tab:hover {
 	left: 0;
 	width: 64px;
 	height: 100vh;
-	background-color: #03a9f4;
+	background-color: $primary-color;
 	box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16),
 		0 2px 10px 0 rgba(0, 0, 0, 0.12);
 
@@ -427,7 +423,7 @@ a.nav-item.is-tab:hover {
 .control-sidebar .sidebar-item {
 	font-size: 2rem;
 	height: 50px;
-	color: white;
+	color: $white;
 	-webkit-box-align: center;
 	-ms-flex-align: center;
 	align-items: center;
@@ -454,7 +450,7 @@ a.nav-item.is-tab:hover {
 	width: 160px;
 	font-size: 12px;
 	background-color: rgba(3, 169, 244, 0.8);
-	color: #fff;
+	color: $white;
 	text-align: center;
 	border-radius: 6px;
 	padding: 5px;

File diff suppressed because it is too large
+ 259 - 279
frontend/components/Station/Station.vue


+ 4 - 3
frontend/components/User/ResetPassword.vue

@@ -86,9 +86,8 @@ export default {
 		};
 	},
 	mounted() {
-		const _this = this;
 		io.getSocket(socket => {
-			_this.socket = socket;
+			this.socket = socket;
 		});
 	},
 	methods: {
@@ -140,12 +139,14 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .container {
 	padding: 25px;
 }
 
 .skip-step {
 	background-color: #7e7e7e;
-	color: #fff;
+	color: $white;
 }
 </style>

+ 33 - 30
frontend/components/User/Settings.vue

@@ -109,7 +109,7 @@
 			<a
 				v-if="!github"
 				class="button is-github"
-				:href="`${$parent.serverDomain}/auth/github/link`"
+				:href="`${serverDomain}/auth/github/link`"
 			>
 				<div class="icon">
 					<img class="invert" src="/assets/social/github.svg" />
@@ -146,6 +146,8 @@
 </template>
 
 <script>
+import { mapState } from "vuex";
+
 import { Toast } from "vue-roaster";
 
 import MainHeader from "../MainHeader.vue";
@@ -164,40 +166,43 @@ export default {
 			github: false,
 			setNewPassword: "",
 			passwordStep: 1,
-			passwordCode: ""
+			passwordCode: "",
+			serverDomain: ""
 		};
 	},
+	computed: mapState({
+		userId: state => state.user.auth.userId
+	}),
 	mounted() {
-		const _this = this;
+		lofig.get("serverDomain", res => {
+			this.serverDomain = res;
+		});
+
 		io.getSocket(socket => {
-			_this.socket = socket;
-			_this.socket.emit("users.findBySession", res => {
+			this.socket = socket;
+			this.socket.emit("users.findBySession", res => {
 				if (res.status === "success") {
-					_this.user = res.data;
-					_this.password = _this.user.password;
-					_this.github = _this.user.github;
+					this.user = res.data;
+					this.password = this.user.password;
+					this.github = this.user.github;
 				} else {
-					_this.$parent.isLoginActive = true;
 					Toast.methods.addToast(
 						"Your are currently not signed in",
 						3000
 					);
 				}
 			});
-			_this.socket.on("event:user.username.changed", username => {
-				_this.$parent.username = username;
+			this.socket.on("event:user.linkPassword", () => {
+				this.password = true;
 			});
-			_this.socket.on("event:user.linkPassword", () => {
-				_this.password = true;
+			this.socket.on("event:user.linkGitHub", () => {
+				this.github = true;
 			});
-			_this.socket.on("event:user.linkGitHub", () => {
-				_this.github = true;
+			this.socket.on("event:user.unlinkPassword", () => {
+				this.password = false;
 			});
-			_this.socket.on("event:user.unlinkPassword", () => {
-				_this.password = false;
-			});
-			_this.socket.on("event:user.unlinkGitHub", () => {
-				_this.github = false;
+			this.socket.on("event:user.unlinkGitHub", () => {
+				this.github = false;
 			});
 		});
 	},
@@ -217,7 +222,7 @@ export default {
 
 			return this.socket.emit(
 				"users.updateEmail",
-				this.$parent.userId,
+				this.userId,
 				email,
 				res => {
 					if (res.status !== "success")
@@ -245,7 +250,7 @@ export default {
 
 			return this.socket.emit(
 				"users.updateUsername",
-				this.$parent.userId,
+				this.userId,
 				username,
 				res => {
 					if (res.status !== "success")
@@ -340,24 +345,22 @@ export default {
 			});
 		},
 		removeSessions() {
-			this.socket.emit(
-				`users.removeSessions`,
-				this.$parent.userId,
-				res => {
-					Toast.methods.addToast(res.message, 4000);
-				}
-			);
+			this.socket.emit(`users.removeSessions`, this.userId, res => {
+				Toast.methods.addToast(res.message, 4000);
+			});
 		}
 	}
 };
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .container {
 	padding: 25px;
 }
 
 a {
-	color: #029ce3 !important;
+	color: $primary-color !important;
 }
 </style>

+ 14 - 9
frontend/components/User/Show.vue

@@ -6,9 +6,7 @@
 			<h2 class="has-text-centered username">@{{ user.username }}</h2>
 			<h5>A member since {{ user.createdAt }}</h5>
 			<div
-				v-if="
-					$parent.role === 'admin' && !($parent.userId === user._id)
-				"
+				v-if="role === 'admin' && userId !== user._id"
 				class="admin-functionality"
 			>
 				<a
@@ -64,6 +62,8 @@
 </template>
 
 <script>
+import { mapState } from "vuex";
+
 import { Toast } from "vue-roaster";
 
 import MainHeader from "../MainHeader.vue";
@@ -78,21 +78,24 @@ export default {
 			isUser: false
 		};
 	},
+	computed: mapState({
+		role: state => state.user.auth.role,
+		userId: state => state.user.auth.userId
+	}),
 	mounted() {
-		const _this = this;
 		io.getSocket(socket => {
-			_this.socket = socket;
-			_this.socket.emit(
+			this.socket = socket;
+			this.socket.emit(
 				"users.findByUsername",
-				_this.$route.params.username,
+				this.$route.params.username,
 				res => {
 					if (res.status === "error") this.$router.go("/404");
 					else {
-						_this.user = res.data;
+						this.user = res.data;
 						this.user.createdAt = moment(
 							this.user.createdAt
 						).format("LL");
-						_this.isUser = true;
+						this.isUser = true;
 					}
 				}
 			);
@@ -120,6 +123,8 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .container {
 	padding: 25px;
 }

+ 9 - 13
frontend/components/UserIdToUsername.vue

@@ -1,36 +1,32 @@
 <template>
 	<router-link
-		v-if="$props.link && username"
-		:to="{ path: `/u/${userIdMap['Z' + $props.userId]}` }"
+		v-if="$props.link && username !== 'unknown'"
+		:to="{ path: `/u/${username}` }"
+		:title="userId"
 	>
-		{{ username ? username : "unknown" }}
+		{{ username }}
 	</router-link>
-	<span v-else>
-		{{ username ? username : "unknown" }}
+	<span :title="userId" v-else>
+		{{ username }}
 	</span>
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
+import { mapActions } from "vuex";
 
 export default {
 	props: ["userId", "link"],
 	data() {
 		return {
-			username: ""
+			username: "unknown"
 		};
 	},
-	computed: {
-		...mapState("user/auth", {
-			userIdMap: state => state.userIdMap
-		})
-	},
 	methods: {
 		...mapActions("user/auth", ["getUsernameFromId"])
 	},
 	mounted() {
 		this.getUsernameFromId(this.$props.userId).then(res => {
-			this.username = res;
+			if (res) this.username = res;
 		});
 	}
 };

+ 2 - 0
frontend/components/pages/About.vue

@@ -77,6 +77,8 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .card {
 	margin-top: 50px;
 }

+ 19 - 17
frontend/components/pages/Admin.vue

@@ -168,39 +168,41 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .tabs {
-	background-color: #ffffff;
+	background-color: $white;
 	.queueSongs {
-		color: #00d1b2;
-		border-color: #00d1b2;
+		color: $teal;
+		border-color: $teal;
 	}
 	.songs {
-		color: #03a9f4;
-		border-color: #03a9f4;
+		color: $primary-color;
+		border-color: $primary-color;
 	}
 	.stations {
-		color: #90298c;
-		border-color: #90298c;
+		color: $purple;
+		border-color: $purple;
 	}
 	.reports {
-		color: #f7c218;
-		border-color: #f7c218;
+		color: $yellow;
+		border-color: $yellow;
 	}
 	.news {
-		color: #e49ba6;
-		border-color: #e49ba6;
+		color: $light-pink;
+		border-color: $light-pink;
 	}
 	.users {
-		color: #ea4962;
-		border-color: #ea4962;
+		color: $dark-pink;
+		border-color: $dark-pink;
 	}
 	.statistics {
-		color: #ff5e00;
-		border-color: #ff5e00;
+		color: $light-orange;
+		border-color: $light-orange;
 	}
 	.punishments {
-		color: #fc3200;
-		border-color: #fc3200;
+		color: $dark-orange;
+		border-color: $dark-orange;
 	}
 	.tab {
 		transition: all 0.2s ease-in-out;

+ 10 - 3
frontend/components/pages/Banned.vue

@@ -3,25 +3,32 @@
 		<i class="material-icons">not_interested</i>
 		<h4>
 			You are banned for
-			<strong>{{ moment($parent.ban.expiresAt).fromNow(true) }}</strong>
+			<strong>{{ moment(ban.expiresAt).fromNow(true) }}</strong>
 		</h4>
 		<h5 class="reason">
 			<strong>Reason: </strong>
-			{{ $parent.ban.reason }}
+			{{ ban.reason }}
 		</h5>
 	</div>
 </template>
 <script>
+import { mapState } from "vuex";
+
 export default {
 	data() {
 		return {
 			moment
 		};
-	}
+	},
+	computed: mapState({
+		ban: state => state.user.auth.ban
+	})
 };
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .container {
 	display: flex;
 	justify-content: center;

+ 152 - 180
frontend/components/pages/Home.vue

@@ -5,63 +5,9 @@
 			<div class="content-wrapper">
 				<div class="group">
 					<div class="group-title">
-						Official Stations
-					</div>
-					<router-link
-						v-for="(station, index) in stations.official"
-						:key="index"
-						class="card station-card"
-						:to="{ name: 'official', params: { id: station.name } }"
-						:class="{ isPrivate: station.privacy === 'private' }"
-					>
-						<div class="card-image">
-							<figure class="image is-square">
-								<img
-									:src="station.currentSong.thumbnail"
-									onerror="this.src='/assets/notes-transparent.png'"
-								/>
-							</figure>
-						</div>
-						<div class="card-content">
-							<div class="media">
-								<div class="media-left displayName">
-									<h5>{{ station.displayName }}</h5>
-								</div>
-							</div>
-
-							<div class="content">
-								{{ station.description }}
-							</div>
-
-							<div class="under-content">
-								<span class="official"
-									><i class="badge material-icons"
-										>verified_user</i
-									>Official</span
-								>
-								<i
-									v-if="station.privacy !== 'public'"
-									class="material-icons right-icon"
-									title="This station is not visible to other users."
-									>lock</i
-								>
-							</div>
-						</div>
-						<router-link
-							href="#"
-							class="absolute-a"
-							:to="{
-								name: 'official',
-								params: { id: station.name }
-							}"
-						/>
-					</router-link>
-				</div>
-				<div class="group">
-					<div class="group-title">
-						Community Stations&nbsp;
+						Stations&nbsp;
 						<a
-							v-if="$parent.loggedIn"
+							v-if="loggedIn"
 							href="#"
 							@click="
 								openModal({
@@ -76,10 +22,10 @@
 						</a>
 					</div>
 					<router-link
-						v-for="(station, index) in stations.community"
+						v-for="(station, index) in filteredStations"
 						:key="index"
 						:to="{
-							name: 'community',
+							name: 'station',
 							params: { id: station.name }
 						}"
 						class="card station-card"
@@ -90,7 +36,24 @@
 					>
 						<div class="card-image">
 							<figure class="image is-square">
+								<div
+									v-if="station.currentSong.ytThumbnail"
+									class="ytThumbnailBg"
+									v-bind:style="{
+										'background-image':
+											'url(' +
+											station.currentSong.ytThumbnail +
+											')'
+									}"
+								></div>
 								<img
+									v-if="station.currentSong.ytThumbnail"
+									:src="station.currentSong.ytThumbnail"
+									onerror="this.src='/assets/notes-transparent.png'"
+									class="ytThumbnail"
+								/>
+								<img
+									v-else
 									:src="station.currentSong.thumbnail"
 									onerror="this.src='/assets/notes-transparent.png'"
 								/>
@@ -99,7 +62,14 @@
 						<div class="card-content">
 							<div class="media">
 								<div class="media-left displayName">
-									<h5>{{ station.displayName }}</h5>
+									<h5>
+										{{ station.displayName }}
+										<i
+											v-if="station.type === 'official'"
+											class="badge material-icons"
+											>verified_user</i
+										>
+									</h5>
 								</div>
 							</div>
 
@@ -110,7 +80,13 @@
 								<span class="hostedby"
 									>Hosted by
 									<span class="host">
+										<span
+											v-if="station.type === 'official'"
+											title="Musare"
+											>Musare</span
+										>
 										<user-id-to-username
+											v-else
 											:userId="station.owner"
 											:link="true"
 										/>
@@ -123,7 +99,10 @@
 									>lock</i
 								>
 								<i
-									v-if="isOwner(station)"
+									v-if="
+										station.type === 'community' &&
+											isOwner(station)
+									"
 									class="material-icons right-icon"
 									title="This is your station."
 									>home</i
@@ -131,10 +110,9 @@
 							</div>
 						</div>
 						<router-link
-							href="#"
 							class="absolute-a"
 							:to="{
-								name: 'community',
+								name: 'station',
 								params: { id: station.name }
 							}"
 						/>
@@ -155,7 +133,6 @@ import MainFooter from "../MainFooter.vue";
 import CreateCommunityStation from "../Modals/CreateCommunityStation.vue";
 import UserIdToUsername from "../UserIdToUsername.vue";
 
-import auth from "../../auth";
 import io from "../../io";
 
 export default {
@@ -164,127 +141,95 @@ export default {
 			recaptcha: {
 				key: ""
 			},
-			stations: {
-				official: [],
-				community: []
-			}
+			stations: [],
+			searchQuery: ""
 		};
 	},
-	computed: mapState("modals", {
-		modals: state => state.modals.home
-	}),
+	computed: {
+		filteredStations() {
+			return this.stations.filter(
+				station =>
+					JSON.stringify(Object.values(station)).indexOf(
+						this.searchQuery
+					) !== -1
+			);
+		},
+		...mapState({
+			modals: state => state.modals.modals.home,
+			loggedIn: state => state.user.auth.loggedIn,
+			userId: state => state.user.auth.userId
+		})
+	},
 	mounted() {
-		const _this = this;
-		auth.getStatus(() => {
-			io.getSocket(socket => {
-				_this.socket = socket;
-				if (_this.socket.connected) _this.init();
-				io.onConnect(() => {
-					_this.init();
-				});
-				_this.socket.on("event:stations.created", res => {
-					const station = res;
-
-					if (!station.currentSong)
-						station.currentSong = {
-							thumbnail: "/assets/notes-transparent.png"
-						};
-					if (station.currentSong && !station.currentSong.thumbnail)
-						station.currentSong.thumbnail =
-							"/assets/notes-transparent.png";
-					_this.stations[station.type].push(station);
-				});
-				_this.socket.on(
-					"event:userCount.updated",
-					(stationId, userCount) => {
-						_this.stations.official.forEach(s => {
-							const station = s;
-							if (station._id === stationId) {
-								station.userCount = userCount;
-							}
-						});
-
-						_this.stations.community.forEach(s => {
-							const station = s;
-							if (station._id === stationId) {
-								station.userCount = userCount;
-							}
-						});
-					}
-				);
-				_this.socket.on("event:station.nextSong", (stationId, song) => {
-					let newSong = song;
-					_this.stations.official.forEach(s => {
-						const station = s;
-						if (station._id === stationId) {
-							if (!newSong)
-								newSong = {
-									thumbnail: "/assets/notes-transparent.png"
-								};
-							if (newSong && !newSong.thumbnail)
-								newSong.thumbnail =
-									"/assets/notes-transparent.png";
-							station.currentSong = newSong;
-						}
-					});
-
-					_this.stations.community.forEach(s => {
+		io.getSocket(socket => {
+			this.socket = socket;
+			if (this.socket.connected) this.init();
+			io.onConnect(() => {
+				this.init();
+			});
+			this.socket.on("event:stations.created", res => {
+				const station = res;
+
+				if (!station.currentSong)
+					station.currentSong = {
+						thumbnail: "/assets/notes-transparent.png"
+					};
+				if (station.currentSong && !station.currentSong.thumbnail)
+					station.currentSong.ytThumbnail = `https://img.youtube.com/vi/${station.currentSong.songId}/mqdefault.jpg`;
+				this.stations.push(station);
+			});
+			this.socket.on(
+				"event:userCount.updated",
+				(stationId, userCount) => {
+					this.stations.forEach(s => {
 						const station = s;
 						if (station._id === stationId) {
-							if (!newSong)
-								newSong = {
-									thumbnail: "/assets/notes-transparent.png"
-								};
-							if (newSong && !newSong.thumbnail)
-								newSong.thumbnail =
-									"/assets/notes-transparent.png";
-							station.currentSong = newSong;
+							station.userCount = userCount;
 						}
 					});
+				}
+			);
+			this.socket.on("event:station.nextSong", (stationId, song) => {
+				let newSong = song;
+				this.stations.forEach(s => {
+					const station = s;
+					if (station._id === stationId) {
+						if (!newSong)
+							newSong = {
+								thumbnail: "/assets/notes-transparent.png"
+							};
+						if (newSong && !newSong.thumbnail)
+							newSong.ytThumbnail = `https://img.youtube.com/vi/${newSong.songId}/mqdefault.jpg`;
+						station.currentSong = newSong;
+					}
 				});
 			});
 		});
 	},
 	methods: {
 		init() {
-			const _this = this;
-			auth.getStatus((authenticated, role, username, userId) => {
-				_this.socket.emit("stations.index", data => {
-					_this.stations.community = [];
-					_this.stations.official = [];
-					if (data.status === "success")
-						data.stations.forEach(s => {
-							const station = s;
-							if (!station.currentSong)
-								station.currentSong = {
-									thumbnail: "/assets/notes-transparent.png"
-								};
-							if (
-								station.currentSong &&
-								!station.currentSong.thumbnail
-							)
-								station.currentSong.thumbnail =
-									"/assets/notes-transparent.png";
-							if (station.privacy !== "public")
-								station.class = { "station-red": true };
-							else if (
-								station.type === "community" &&
-								station.owner === userId
-							)
-								station.class = { "station-blue": true };
-							if (station.type === "official")
-								_this.stations.official.push(station);
-							else _this.stations.community.push(station);
-						});
-				});
-				_this.socket.emit("apis.joinRoom", "home", () => {});
+			this.socket.emit("stations.index", data => {
+				this.stations = [];
+				if (data.status === "success")
+					data.stations.forEach(s => {
+						const station = s;
+						if (!station.currentSong)
+							station.currentSong = {
+								thumbnail: "/assets/notes-transparent.png"
+							};
+						if (
+							station.currentSong &&
+							!station.currentSong.thumbnail
+						)
+							station.currentSong.ytThumbnail = `https://img.youtube.com/vi/${station.currentSong.songId}/mqdefault.jpg`;
+						this.stations.push(station);
+					});
 			});
+			this.socket.emit("apis.joinRoom", "home", () => {});
 		},
 		isOwner(station) {
-			const _this = this;
 			return (
-				station.owner === _this.$parent.userId &&
-				station.privacy === "public"
+				station.owner === this.userId && station.privacy === "public"
 			);
 		},
 		...mapActions("modals", ["openModal"])
@@ -299,6 +244,8 @@ export default {
 </script>
 
 <style lang="scss">
+@import "styles/global.scss";
+
 * {
 	box-sizing: border-box;
 }
@@ -306,7 +253,7 @@ export default {
 html {
 	width: 100%;
 	height: 100%;
-	color: rgba(0, 0, 0, 0.87);
+	color: $dark-grey-2;
 
 	body {
 		width: 100%;
@@ -354,26 +301,19 @@ html {
 
 	.official {
 		font-size: 18px;
-		color: #03a9f4;
+		color: $primary-color;
 		position: relative;
 		top: -5px;
-
-		.badge {
-			position: relative;
-			padding-right: 2px;
-			color: #38d227;
-			top: +5px;
-		}
 	}
 
 	.hostedby {
 		font-size: 15px;
 
 		.host {
-			color: #03a9f4;
+			color: $primary-color;
 
 			a {
-				color: #03a9f4;
+				color: $primary-color;
 			}
 		}
 	}
@@ -401,6 +341,7 @@ html {
 	margin: 10px;
 	cursor: pointer;
 	height: 475px;
+	background: $white;
 
 	transition: all ease-in-out 0.2s;
 
@@ -418,6 +359,30 @@ html {
 			max-height: 60px;
 		}
 	}
+
+	.card-image {
+		height: 300px;
+		width: 300px;
+		overflow: hidden;
+		.image {
+			.ytThumbnailBg {
+				background: url("/assets/notes-transparent.png") no-repeat
+					center center;
+				background-size: cover;
+				height: 300px;
+				width: 300px;
+				position: absolute;
+				top: 0;
+				filter: blur(5px);
+			}
+			.ytThumbnail {
+				height: auto;
+				top: 0;
+				margin-top: auto;
+				margin-bottom: auto;
+			}
+		}
+	}
 }
 
 .station-card:hover {
@@ -437,11 +402,11 @@ html {
 	cursor: pointer;
 	transition: 0.25s ease color;
 	font-size: 30px;
-	color: #4a4a4a;
+	color: $dark-grey;
 }
 
 .community-button:hover {
-	color: #03a9f4;
+	color: $primary-color;
 }
 
 .station-privacy {
@@ -509,5 +474,12 @@ html {
 	-webkit-line-clamp: 1;
 	line-height: 30px;
 	max-height: 30px;
+
+	.badge {
+		position: relative;
+		padding-right: 2px;
+		color: $green;
+		top: +5px;
+	}
 }
 </style>

+ 17 - 16
frontend/components/pages/News.vue

@@ -90,27 +90,26 @@ export default {
 		};
 	},
 	mounted() {
-		const _this = this;
 		io.getSocket(socket => {
-			_this.socket = socket;
-			_this.socket.emit("news.index", res => {
-				_this.news = res.data;
-				if (_this.news.length === 0) _this.noFound = true;
+			this.socket = socket;
+			this.socket.emit("news.index", res => {
+				this.news = res.data;
+				if (this.news.length === 0) this.noFound = true;
 			});
-			_this.socket.on("event:admin.news.created", news => {
-				_this.news.unshift(news);
-				_this.noFound = false;
+			this.socket.on("event:admin.news.created", news => {
+				this.news.unshift(news);
+				this.noFound = false;
 			});
-			_this.socket.on("event:admin.news.updated", news => {
-				for (let n = 0; n < _this.news.length; n += 1) {
-					if (_this.news[n]._id === news._id) {
-						_this.news.$set(n, news);
+			this.socket.on("event:admin.news.updated", news => {
+				for (let n = 0; n < this.news.length; n += 1) {
+					if (this.news[n]._id === news._id) {
+						this.news.$set(n, news);
 					}
 				}
 			});
-			_this.socket.on("event:admin.news.removed", news => {
-				_this.news = _this.news.filter(item => item._id !== news._id);
-				if (_this.news.length === 0) _this.noFound = true;
+			this.socket.on("event:admin.news.removed", news => {
+				this.news = this.news.filter(item => item._id !== news._id);
+				if (this.news.length === 0) this.noFound = true;
 			});
 		});
 	},
@@ -123,6 +122,8 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 .card {
 	margin-top: 50px;
 }
@@ -133,7 +134,7 @@ export default {
 		padding: 12px;
 		text-transform: uppercase;
 		font-weight: bold;
-		color: #fff;
+		color: $white;
 	}
 
 	.sect-head-features {

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

@@ -202,6 +202,8 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "styles/global.scss";
+
 li a {
 	color: dodgerblue;
 	border-bottom: 0 !important;
@@ -245,7 +247,7 @@ ul {
 }
 
 .custom-tag.purple {
-	border-bottom: 2px #90298c solid;
+	border-bottom: 2px $purple solid;
 }
 
 .thanks {

+ 0 - 0
frontend/dist/android-chrome-144x144.png → frontend/dist/assets/favicon/android-chrome-144x144.png


+ 0 - 0
frontend/dist/android-chrome-192x192.png → frontend/dist/assets/favicon/android-chrome-192x192.png


+ 0 - 0
frontend/dist/android-chrome-36x36.png → frontend/dist/assets/favicon/android-chrome-36x36.png


+ 0 - 0
frontend/dist/android-chrome-48x48.png → frontend/dist/assets/favicon/android-chrome-48x48.png


+ 0 - 0
frontend/dist/android-chrome-72x72.png → frontend/dist/assets/favicon/android-chrome-72x72.png


+ 0 - 0
frontend/dist/android-chrome-96x96.png → frontend/dist/assets/favicon/android-chrome-96x96.png


+ 0 - 0
frontend/dist/apple-touch-icon-114x114.png → frontend/dist/assets/favicon/apple-touch-icon-114x114.png


+ 0 - 0
frontend/dist/apple-touch-icon-120x120.png → frontend/dist/assets/favicon/apple-touch-icon-120x120.png


+ 0 - 0
frontend/dist/apple-touch-icon-144x144.png → frontend/dist/assets/favicon/apple-touch-icon-144x144.png


+ 0 - 0
frontend/dist/apple-touch-icon-152x152.png → frontend/dist/assets/favicon/apple-touch-icon-152x152.png


+ 0 - 0
frontend/dist/apple-touch-icon-180x180.png → frontend/dist/assets/favicon/apple-touch-icon-180x180.png


+ 0 - 0
frontend/dist/apple-touch-icon-57x57.png → frontend/dist/assets/favicon/apple-touch-icon-57x57.png


+ 0 - 0
frontend/dist/apple-touch-icon-60x60.png → frontend/dist/assets/favicon/apple-touch-icon-60x60.png


+ 0 - 0
frontend/dist/apple-touch-icon-72x72.png → frontend/dist/assets/favicon/apple-touch-icon-72x72.png


+ 0 - 0
frontend/dist/apple-touch-icon-76x76.png → frontend/dist/assets/favicon/apple-touch-icon-76x76.png


+ 0 - 0
frontend/dist/apple-touch-icon-precomposed.png → frontend/dist/assets/favicon/apple-touch-icon-precomposed.png


+ 0 - 0
frontend/dist/apple-touch-icon.png → frontend/dist/assets/favicon/apple-touch-icon.png


+ 0 - 0
frontend/dist/favicon-16x16.png → frontend/dist/assets/favicon/favicon-16x16.png


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