Quellcode durchsuchen

Merge pull request #42 from Musare/experimental

Lofig, vue-roaster, EditStation modal, bugs...
Jonathan Graham vor 5 Jahren
Ursprung
Commit
880397ea3b
68 geänderte Dateien mit 2496 neuen und 2190 gelöschten Zeilen
  1. 1 1
      .gitignore
  2. 66 64
      README.md
  3. 3 1
      backend/core.js
  4. 1 1
      backend/index.js
  5. 4 4
      backend/logic/actions/apis.js
  6. 0 1
      backend/logic/actions/hooks/adminRequired.js
  7. 0 1
      backend/logic/actions/hooks/loginRequired.js
  8. 0 1
      backend/logic/actions/hooks/ownerRequired.js
  9. 8 9
      backend/logic/actions/news.js
  10. 53 65
      backend/logic/actions/playlists.js
  11. 6 7
      backend/logic/actions/punishments.js
  12. 37 37
      backend/logic/actions/queueSongs.js
  13. 7 9
      backend/logic/actions/reports.js
  14. 26 34
      backend/logic/actions/songs.js
  15. 62 101
      backend/logic/actions/stations.js
  16. 49 59
      backend/logic/actions/users.js
  17. 1 1
      backend/logic/cache/index.js
  18. 39 30
      backend/logic/db/index.js
  19. 1 1
      backend/logic/discord.js
  20. 8 3
      backend/logic/io.js
  21. 2 1
      backend/logic/notifications.js
  22. 57 27
      backend/logic/stations.js
  23. 33 11
      backend/logic/tasks.js
  24. 14 34
      backend/logic/utils.js
  25. 2 1
      frontend/.eslintrc
  26. 0 16
      frontend/.snyk
  27. 17 8
      frontend/App.vue
  28. 5 5
      frontend/api/auth.js
  29. 0 5
      frontend/components/404.vue
  30. 0 501
      frontend/components/Admin/EditStation.vue
  31. 26 18
      frontend/components/Admin/News.vue
  32. 2 2
      frontend/components/Admin/Punishments.vue
  33. 53 34
      frontend/components/Admin/QueueSongs.vue
  34. 21 9
      frontend/components/Admin/Reports.vue
  35. 46 9
      frontend/components/Admin/Songs.vue
  36. 33 21
      frontend/components/Admin/Stations.vue
  37. 3 3
      frontend/components/MainFooter.vue
  38. 7 9
      frontend/components/MainHeader.vue
  39. 3 3
      frontend/components/Modals/AddSongToPlaylist.vue
  40. 25 13
      frontend/components/Modals/AddSongToQueue.vue
  41. 39 35
      frontend/components/Modals/CreateCommunityStation.vue
  42. 11 4
      frontend/components/Modals/EditNews.vue
  43. 139 98
      frontend/components/Modals/EditSong.vue
  44. 869 174
      frontend/components/Modals/EditStation.vue
  45. 36 30
      frontend/components/Modals/EditUser.vue
  46. 29 6
      frontend/components/Modals/IssuesModal.vue
  47. 37 33
      frontend/components/Modals/Login.vue
  48. 13 11
      frontend/components/Modals/Playlists/Create.vue
  49. 25 22
      frontend/components/Modals/Playlists/Edit.vue
  50. 8 8
      frontend/components/Modals/Register.vue
  51. 2 2
      frontend/components/Modals/Report.vue
  52. 6 3
      frontend/components/Sidebars/Playlist.vue
  53. 7 6
      frontend/components/Sidebars/SongsList.vue
  54. 123 67
      frontend/components/Station/Station.vue
  55. 9 10
      frontend/components/Station/StationHeader.vue
  56. 16 7
      frontend/components/User/ResetPassword.vue
  57. 67 58
      frontend/components/User/Settings.vue
  58. 6 6
      frontend/components/User/Show.vue
  59. 293 287
      frontend/components/pages/Home.vue
  60. 0 0
      frontend/dist/assets/blue_wordmark.png
  61. BIN
      frontend/dist/assets/white_wordmark.png
  62. 2 1
      frontend/dist/config/template.json
  63. 4 1
      frontend/dist/index.tpl.html
  64. 0 0
      frontend/dist/lofig.min.js
  65. 16 12
      frontend/main.js
  66. 1 1
      frontend/package.json
  67. 4 2
      frontend/validation.js
  68. 13 146
      yarn.lock

+ 1 - 1
.gitignore

@@ -13,6 +13,7 @@ startMongo.cmd
 .redis
 .redis
 *.rdb
 *.rdb
 npm-debug.log
 npm-debug.log
+lerna-debug.log
 
 
 # Backend
 # Backend
 backend/node_modules/
 backend/node_modules/
@@ -24,7 +25,6 @@ frontend/bundle-stats.json
 frontend/bundle-report.html
 frontend/bundle-report.html
 frontend/node_modules/
 frontend/node_modules/
 frontend/dist/build/
 frontend/dist/build/
-!frontend/dist/lofig.min.js
 frontend/dist/index.html
 frontend/dist/index.html
 frontend/dist/config/default.json
 frontend/dist/config/default.json
 
 

+ 66 - 64
README.md

@@ -1,5 +1,4 @@
 
 
-  
 # MusareNode
 # MusareNode
 
 
 Based off of the original [Musare](https://github.com/Musare/MusareMeteor), which utilized Meteor.
 Based off of the original [Musare](https://github.com/Musare/MusareMeteor), which utilized Meteor.
@@ -34,6 +33,7 @@ The backend is a scalable NodeJS / Redis / MongoDB app. Each backend server hand
 We currently only utilize 1 backend, 1 MongoDB server and 1 Redis server running for production, though it is relatively easy to expand.
 We currently only utilize 1 backend, 1 MongoDB server and 1 Redis server running for production, though it is relatively easy to expand.
 
 
 ## Requirements
 ## Requirements
+
 Installing with Docker: (not recommended for Windows users)
 Installing with Docker: (not recommended for Windows users)
 
 
 - [Docker](https://www.docker.com/)
 - [Docker](https://www.docker.com/)
@@ -57,56 +57,57 @@ Once you've installed the required tools:
 
 
 3. `cp backend/config/template.json backend/config/default.json`
 3. `cp backend/config/template.json backend/config/default.json`
 
 
-|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.|
-|`apis.discogs`|Can be obtained by setting up a [Discogs application](https://www.discogs.com/settings/developers), 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.|
+   |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.|
+   |`apis.discogs`|Can be obtained by setting up a [Discogs application](https://www.discogs.com/settings/developers), 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`
 4. `cp frontend/build/config/template.json frontend/build/config/default.json`
 
 
-|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.|
+   |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.|
 
 
 5. Simply `cp .env.example .env` to setup your environment variables.
 5. Simply `cp .env.example .env` to setup your environment variables.
 
 
 6. To setup [snyk](https://snyk.io/) (which is what we use for our precommit git-hooks), you will need to:
 6. To setup [snyk](https://snyk.io/) (which is what we use for our precommit git-hooks), you will need to:
-- Setup an account
-- Go to [settings](https://app.snyk.io/account)
-- Copy the API token and set it as your `SNYK_TOKEN` environment variable.
+
+   - Setup an account
+   - Go to [settings](https://app.snyk.io/account)
+   - Copy the API token and set it as your `SNYK_TOKEN` environment variable.
 
 
 We use snyk to test our dependencies / dev-dependencies for vulnerabilities.
 We use snyk to test our dependencies / dev-dependencies for vulnerabilities.
 
 
 ### Installing with Docker
 ### Installing with Docker
 
 
-_Configuration_
+#### Configuration
 
 
 To configure docker configure the `.env` file to match your settings in `backend/config/default.json`.  
 To configure docker configure the `.env` file to match your settings in `backend/config/default.json`.  
 The configurable ports will be how you access the services on your machine, or what ports you will need to specify in your nginx files when using proxy_pass. 
 The configurable ports will be how you access the services on your machine, or what ports you will need to specify in your nginx files when using proxy_pass. 
@@ -127,20 +128,19 @@ The configurable ports will be how you access the services on your machine, or w
 
 
       In `.env` set the environment variable of `MONGO_USER_USERNAME` and `MONGO_USER_PASSWORD`.
       In `.env` set the environment variable of `MONGO_USER_USERNAME` and `MONGO_USER_PASSWORD`.
 
 
-   2. Start the database (in detached mode), which will generate the correct MongoDB users.
+   3. Start the database (in detached mode), which will generate the correct MongoDB users.
 
 
       `docker-compose up -d mongo`
       `docker-compose up -d mongo`
 
 
-
-3) Start redis and the mongo client in the background, as we usually don't need to monitor these for errors
+3. Start redis and the mongo client in the background, as we usually don't need to monitor these for errors
 
 
    `docker-compose up -d mongoclient redis`
    `docker-compose up -d mongoclient redis`
 
 
-4) Start the backend and frontend in the foreground, so we can watch for errors during development
+4. Start the backend and frontend in the foreground, so we can watch for errors during development
 
 
    `docker-compose up backend frontend`
    `docker-compose up backend frontend`
 
 
-5) You should now be able to begin development! The backend is auto reloaded when
+5. You should now be able to begin development! The backend is auto reloaded when
    you make changes and the frontend is auto compiled and live reloaded by webpack
    you make changes and the frontend is auto compiled and live reloaded by webpack
    when you make changes. You should be able to access Musare in your local browser
    when you make changes. You should be able to access Musare in your local browser
    at `http://<docker-machine-ip>:8080/` where `<docker-machine-ip>` can be found below:
    at `http://<docker-machine-ip>:8080/` where `<docker-machine-ip>` can be found below:
@@ -150,24 +150,25 @@ The configurable ports will be how you access the services on your machine, or w
    - Docker ToolBox: The output of `docker-machine ip default`
    - Docker ToolBox: The output of `docker-machine ip default`
 
 
 If you are using linting extensions in IDEs/want to run `yarn lint`, you need to install the following locally (outside of Docker):
 If you are using linting extensions in IDEs/want to run `yarn lint`, you need to install the following locally (outside of Docker):
-```
-yarn global add eslint
-yarn add eslint-config-airbnb-base
-```
+
+   ```bash
+   yarn global add eslint
+   yarn add eslint-config-airbnb-base
+   ```
 
 
 ### Standard Installation
 ### Standard Installation
 
 
 Steps 1-4 are things you only have to do once. The steps to start servers follow.
 Steps 1-4 are things you only have to do once. The steps to start servers follow.
 
 
-1.  In the main folder, create a folder called `.database`
+1. In the main folder, create a folder called `.database`
 
 
-2.  Create a file called `startMongo.cmd` in the main folder with the contents:
+2. Create a file called `startMongo.cmd` in the main folder with the contents:
 
 
-        "C:\Program Files\MongoDB\Server\3.2\bin\mongod.exe" --dbpath "D:\Programming\HTML\MusareNode\.database"
+   "C:\Program Files\MongoDB\Server\3.2\bin\mongod.exe" --dbpath "D:\Programming\HTML\MusareNode\.database"
 
 
-    Make sure to adjust your paths accordingly.
+   Make sure to adjust your paths accordingly.
 
 
-3.  Set up the MongoDB database
+3. Set up the MongoDB database
 
 
     1. Start the database by executing the script `startMongo.cmd` you just made
     1. Start the database by executing the script `startMongo.cmd` you just made
 
 
@@ -195,21 +196,21 @@ Steps 1-4 are things you only have to do once. The steps to start servers follow
 
 
        In `startMongo.cmd` add `--auth` at the end of the first line
        In `startMongo.cmd` add `--auth` at the end of the first line
 
 
-4.  In the folder where you installed Redis, edit the `redis.windows.conf` file. In there, look for the property `notify-keyspace-events`. Make sure that property is uncommented and has the value `Ex`. It should look like `notify-keyspace-events Ex` when done.
+4. In the folder where you installed Redis, edit the `redis.windows.conf` file. In there, look for the property `notify-keyspace-events`. Make sure that property is uncommented and has the value `Ex`. It should look like `notify-keyspace-events Ex` when done.
 
 
-5.  Create a file called `startRedis.cmd` in the main folder with the contents:
+5. Create a file called `startRedis.cmd` in the main folder with the contents:
 
 
-        "D:\Redis\redis-server.exe" "D:\Redis\redis.windows.conf" "--requirepass" "PASSWORD"
+   "D:\Redis\redis-server.exe" "D:\Redis\redis.windows.conf" "--requirepass" "PASSWORD"
 
 
-    And again, make sure that the paths lead to the proper config and executable. Replace `PASSWORD` with your Redis password.
+   And again, make sure that the paths lead to the proper config and executable. Replace `PASSWORD` with your Redis password.
 
 
 ### Non-docker start servers
 ### Non-docker start servers
 
 
-**Automatic**
+#### Automatic
 
 
-1.  If you are on Windows you can run `windows-start.cmd` or just double click the `windows-start.cmd` file and all servers will automatically start up.
+1. If you are on Windows you can run `windows-start.cmd` or just double click the `windows-start.cmd` file and all servers will automatically start up.
 
 
-**Manual**
+#### Manual
 
 
 1. Run `startRedis.cmd` and `startMongo.cmd` to start Redis and Mongo.
 1. Run `startRedis.cmd` and `startMongo.cmd` to start Redis and Mongo.
 
 
@@ -289,15 +290,15 @@ Run this command in your shell. You will have to do this command for every shell
 You can call Toasts using our custom package, [`vue-roaster`](https://github.com/atjonathan/vue-roaster), using the following code:
 You can call Toasts using our custom package, [`vue-roaster`](https://github.com/atjonathan/vue-roaster), using the following code:
 
 
 ```js
 ```js
-import { Toast } from "vue-roaster";
-Toast.methods.addToast("", 0);
+import Toast from "vue-roaster";
+new Toast({ content: "", persistant: true });
 ```
 ```
 
 
 ### Set user role
 ### Set user role
 
 
 When setting up you will need to grant yourself the admin role, using the following commands:
 When setting up you will need to grant yourself the admin role, using the following commands:
 
 
-```
+```bash
 docker-compose exec mongo mongo admin
 docker-compose exec mongo mongo admin
 
 
 use musare
 use musare
@@ -310,6 +311,7 @@ db.users.update({username: "USERNAME"}, {$set: {role: "admin"}})
 We use lerna to add an additional package to either the frontend or the backend.
 We use lerna to add an additional package to either the frontend or the backend.
 
 
 For example, this is how we would to add the `webpack-bundle-analyser` package as a dev-dependency to the frontend:
 For example, this is how we would to add the `webpack-bundle-analyser` package as a dev-dependency to the frontend:
-```
+
+```bash
 npx lerna add webpack-bundle-analyser --scope=musare-frontend --dev
 npx lerna add webpack-bundle-analyser --scope=musare-frontend --dev
 ```
 ```

+ 3 - 1
backend/core.js

@@ -2,6 +2,8 @@ const EventEmitter = require('events');
 
 
 const bus = new EventEmitter();
 const bus = new EventEmitter();
 
 
+bus.setMaxListeners(1000);
+
 module.exports = class {
 module.exports = class {
 	constructor(name, moduleManager) {
 	constructor(name, moduleManager) {
 		this.name = name;
 		this.name = name;
@@ -69,7 +71,7 @@ module.exports = class {
 	}
 	}
 
 
 	_validateHook() {
 	_validateHook() {
-		return Promise.race([this._onInitialize, this._isInitialized]).then(
+		return Promise.race([this._onInitialize(), this._isInitialized()]).then(
 			() => this._isNotLocked()
 			() => this._isNotLocked()
 		);
 		);
 	}
 	}

+ 1 - 1
backend/index.js

@@ -66,7 +66,7 @@ class ModuleManager {
 	}
 	}
 
 
 	async printStatus() {
 	async printStatus() {
-		try { await Promise.race([this.logger._onInitialize, this.logger._isInitialized]); } catch { return; }
+		try { await Promise.race([this.logger._onInitialize(), this.logger._isInitialized()]); } catch { return; }
 		if (!this.fancyConsole) return;
 		if (!this.fancyConsole) return;
 		
 		
 		let colors = this.logger.colors;
 		let colors = this.logger.colors;

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

@@ -58,13 +58,13 @@ module.exports = {
 	 * @param artist - an artist for that song
 	 * @param artist - an artist for that song
 	 * @param cb
 	 * @param cb
 	 */
 	 */
-	getSpotifySongs: hooks.adminRequired((session, title, artist, cb, userId) => {
+	getSpotifySongs: hooks.adminRequired((session, title, artist, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				utils.getSongsFromSpotify(title, artist, next);
 				utils.getSongsFromSpotify(title, artist, next);
 			}
 			}
 		], (songs) => {
 		], (songs) => {
-			logger.success('APIS_GET_SPOTIFY_SONGS', `User "${userId}" got Spotify songs for title "${title}" successfully.`);
+			logger.success('APIS_GET_SPOTIFY_SONGS', `User "${session.userId}" got Spotify songs for title "${title}" successfully.`);
 			cb({status: 'success', songs: songs});
 			cb({status: 'success', songs: songs});
 		});
 		});
 	}),
 	}),
@@ -76,7 +76,7 @@ module.exports = {
 	 * @param query - the query
 	 * @param query - the query
 	 * @param cb
 	 * @param cb
 	 */
 	 */
-	searchDiscogs: hooks.adminRequired((session, query, page, cb, userId) => {
+	searchDiscogs: hooks.adminRequired((session, query, page, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				const params = [
 				const params = [
@@ -106,7 +106,7 @@ module.exports = {
 				logger.error("APIS_SEARCH_DISCOGS", `Searching discogs failed with query "${query}". "${err}"`);
 				logger.error("APIS_SEARCH_DISCOGS", `Searching discogs failed with query "${query}". "${err}"`);
 				return cb({status: 'failure', message: err});
 				return cb({status: 'failure', message: err});
 			}
 			}
-			logger.success('APIS_SEARCH_DISCOGS', `User "${userId}" searched Discogs succesfully for query "${query}".`);
+			logger.success('APIS_SEARCH_DISCOGS', `User "${session.userId}" searched Discogs succesfully for query "${query}".`);
 			cb({status: 'success', results: body.results, pages: body.pagination.pages});
 			cb({status: 'success', results: body.results, pages: body.pagination.pages});
 		});
 		});
 	}),
 	}),

+ 0 - 1
backend/logic/actions/hooks/adminRequired.js

@@ -33,7 +33,6 @@ module.exports = function(next) {
 				return cb({status: 'failure', message: err});
 				return cb({status: 'failure', message: err});
 			}
 			}
 			logger.info("ADMIN_REQUIRED", `User "${session.userId}" passed admin required check.`, false);
 			logger.info("ADMIN_REQUIRED", `User "${session.userId}" passed admin required check.`, false);
-			args.push(session.userId);
 			next.apply(null, args);
 			next.apply(null, args);
 		});
 		});
 	}
 	}

+ 0 - 1
backend/logic/actions/hooks/loginRequired.js

@@ -27,7 +27,6 @@ module.exports = function(next) {
 				return cb({status: 'failure', message: err});
 				return cb({status: 'failure', message: err});
 			}
 			}
 			logger.info("LOGIN_REQUIRED", `User "${session.userId}" passed login required check.`, false);
 			logger.info("LOGIN_REQUIRED", `User "${session.userId}" passed login required check.`, false);
-			args.push(session.userId);
 			next.apply(null, args);
 			next.apply(null, args);
 		});
 		});
 	}
 	}

+ 0 - 1
backend/logic/actions/hooks/ownerRequired.js

@@ -39,7 +39,6 @@ module.exports = function(next) {
 				return cb({status: 'failure', message: err});
 				return cb({status: 'failure', message: err});
 			}
 			}
 			logger.info("OWNER_REQUIRED", `User "${session.userId}" passed owner required check for station "${stationId}"`, false);
 			logger.info("OWNER_REQUIRED", `User "${session.userId}" passed owner required check for station "${stationId}"`, false);
-			args.push(session.userId);
 			next.apply(null, args);
 			next.apply(null, args);
 		});
 		});
 	}
 	}

+ 8 - 9
backend/logic/actions/news.js

@@ -64,12 +64,11 @@ module.exports = {
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Object} data - the object of the news data
 	 * @param {Object} data - the object of the news data
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	create: hooks.adminRequired((session, data, cb, userId) => {
+	create: hooks.adminRequired((session, data, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				data.createdBy = userId;
+				data.createdBy = session.userId;
 				data.createdAt = Date.now();
 				data.createdAt = Date.now();
 				db.models.news.create(data, next);
 				db.models.news.create(data, next);
 			}
 			}
@@ -116,15 +115,15 @@ module.exports = {
 	 */
 	 */
 	//TODO Pass in an id, not an object
 	//TODO Pass in an id, not an object
 	//TODO Fix this
 	//TODO Fix this
-	remove: hooks.adminRequired((session, news, cb, userId) => {
+	remove: hooks.adminRequired((session, news, cb) => {
 		db.models.news.deleteOne({ _id: news._id }, async err => {
 		db.models.news.deleteOne({ _id: news._id }, async err => {
 			if (err) {
 			if (err) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("NEWS_REMOVE", `Removing news "${news._id}" failed for user "${userId}". "${err}"`);
+				logger.error("NEWS_REMOVE", `Removing news "${news._id}" failed for user "${session.userId}". "${err}"`);
 				return cb({ 'status': 'failure', 'message': err });
 				return cb({ 'status': 'failure', 'message': err });
 			} else {
 			} else {
 				cache.pub('news.remove', news);
 				cache.pub('news.remove', news);
-				logger.success("NEWS_REMOVE", `Removing news "${news._id}" successful by user "${userId}".`);
+				logger.success("NEWS_REMOVE", `Removing news "${news._id}" successful by user "${session.userId}".`);
 				return cb({ 'status': 'success', 'message': 'Successfully removed News' });
 				return cb({ 'status': 'success', 'message': 'Successfully removed News' });
 			}
 			}
 		});
 		});
@@ -139,15 +138,15 @@ module.exports = {
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
 	 */
 	 */
 	//TODO Fix this
 	//TODO Fix this
-	update: hooks.adminRequired((session, _id, news, cb, userId) => {
+	update: hooks.adminRequired((session, _id, news, cb) => {
 		db.models.news.updateOne({ _id }, news, { upsert: true }, async err => {
 		db.models.news.updateOne({ _id }, news, { upsert: true }, async err => {
 			if (err) {
 			if (err) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("NEWS_UPDATE", `Updating news "${_id}" failed for user "${userId}". "${err}"`);
+				logger.error("NEWS_UPDATE", `Updating news "${_id}" failed for user "${session.userId}". "${err}"`);
 				return cb({ 'status': 'failure', 'message': err });
 				return cb({ 'status': 'failure', 'message': err });
 			} else {
 			} else {
 				cache.pub('news.update', news);
 				cache.pub('news.update', news);
-				logger.success("NEWS_UPDATE", `Updating news "${_id}" successful for user "${userId}".`);
+				logger.success("NEWS_UPDATE", `Updating news "${_id}" successful for user "${session.userId}".`);
 				return cb({ 'status': 'success', 'message': 'Successfully updated News' });
 				return cb({ 'status': 'success', 'message': 'Successfully updated News' });
 			}
 			}
 		});
 		});

+ 53 - 65
backend/logic/actions/playlists.js

@@ -80,25 +80,24 @@ let lib = {
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {String} playlistId - the id of the playlist we are getting the first song from
 	 * @param {String} playlistId - the id of the playlist we are getting the first song from
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	getFirstSong: hooks.loginRequired((session, playlistId, cb, userId) => {
+	getFirstSong: hooks.loginRequired((session, playlistId, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				playlists.getPlaylist(playlistId, next);
 				playlists.getPlaylist(playlistId, next);
 			},
 			},
 
 
 			(playlist, next) => {
 			(playlist, next) => {
-				if (!playlist || playlist.createdBy !== userId) return next('Playlist not found.');
+				if (!playlist || playlist.createdBy !== session.userId) return next('Playlist not found.');
 				next(null, playlist.songs[0]);
 				next(null, playlist.songs[0]);
 			}
 			}
 		], async (err, song) => {
 		], async (err, song) => {
 			if (err) {
 			if (err) {
 				err = await 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}"`);
+				logger.error("PLAYLIST_GET_FIRST_SONG", `Getting the first song of playlist "${playlistId}" failed for user "${session.userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 				return cb({ status: 'failure', message: err});
 			}
 			}
-			logger.success("PLAYLIST_GET_FIRST_SONG", `Successfully got the first song of playlist "${playlistId}" for user "${userId}".`);
+			logger.success("PLAYLIST_GET_FIRST_SONG", `Successfully got the first song of playlist "${playlistId}" for user "${session.userId}".`);
 			cb({
 			cb({
 				status: 'success',
 				status: 'success',
 				song: song
 				song: song
@@ -111,20 +110,19 @@ let lib = {
 	 *
 	 *
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	indexForUser: hooks.loginRequired((session, cb, userId) => {
+	indexForUser: hooks.loginRequired((session, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				db.models.playlist.find({ createdBy: userId }, next);
+				db.models.playlist.find({ createdBy: session.userId }, next);
 			}
 			}
 		], async (err, playlists) => {
 		], async (err, playlists) => {
 			if (err) {
 			if (err) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("PLAYLIST_INDEX_FOR_USER", `Indexing playlists for user "${userId}" failed. "${err}"`);
+				logger.error("PLAYLIST_INDEX_FOR_USER", `Indexing playlists for user "${session.userId}" failed. "${err}"`);
 				return cb({ status: 'failure', message: err});
 				return cb({ status: 'failure', message: err});
 			}
 			}
-			logger.success("PLAYLIST_INDEX_FOR_USER", `Successfully indexed playlists for user "${userId}".`);
+			logger.success("PLAYLIST_INDEX_FOR_USER", `Successfully indexed playlists for user "${session.userId}".`);
 			cb({
 			cb({
 				status: 'success',
 				status: 'success',
 				data: playlists
 				data: playlists
@@ -138,9 +136,8 @@ let lib = {
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Object} data - the data for the new private playlist
 	 * @param {Object} data - the data for the new private playlist
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	create: hooks.loginRequired((session, data, cb, userId) => {
+	create: hooks.loginRequired((session, data, cb) => {
 		async.waterfall([
 		async.waterfall([
 
 
 			(next) => {
 			(next) => {
@@ -152,7 +149,7 @@ let lib = {
 				db.models.playlist.create({
 				db.models.playlist.create({
 					displayName,
 					displayName,
 					songs,
 					songs,
-					createdBy: userId,
+					createdBy: session.userId,
 					createdAt: Date.now()
 					createdAt: Date.now()
 				}, next);
 				}, next);
 			}
 			}
@@ -160,11 +157,11 @@ let lib = {
 		], async (err, playlist) => {
 		], async (err, playlist) => {
 			if (err) {
 			if (err) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("PLAYLIST_CREATE", `Creating private playlist failed for user "${userId}". "${err}"`);
+				logger.error("PLAYLIST_CREATE", `Creating private playlist failed for user "${session.userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 				return cb({ status: 'failure', message: err});
 			}
 			}
 			cache.pub('playlist.create', playlist._id);
 			cache.pub('playlist.create', playlist._id);
-			logger.success("PLAYLIST_CREATE", `Successfully created private playlist for user "${userId}".`);
+			logger.success("PLAYLIST_CREATE", `Successfully created private playlist for user "${session.userId}".`);
 			cb({ status: 'success', message: 'Successfully created playlist', data: {
 			cb({ status: 'success', message: 'Successfully created playlist', data: {
 				_id: playlist._id
 				_id: playlist._id
 			} });
 			} });
@@ -177,25 +174,24 @@ let lib = {
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {String} playlistId - the id of the playlist we are getting
 	 * @param {String} playlistId - the id of the playlist we are getting
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	getPlaylist: hooks.loginRequired((session, playlistId, cb, userId) => {
+	getPlaylist: hooks.loginRequired((session, playlistId, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				playlists.getPlaylist(playlistId, next);
 				playlists.getPlaylist(playlistId, next);
 			},
 			},
 
 
 			(playlist, next) => {
 			(playlist, next) => {
-				if (!playlist || playlist.createdBy !== userId) return next('Playlist not found');
+				if (!playlist || playlist.createdBy !== session.userId) return next('Playlist not found');
 				next(null, playlist);
 				next(null, playlist);
 			}
 			}
 		], async (err, playlist) => {
 		], async (err, playlist) => {
 			if (err) {
 			if (err) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("PLAYLIST_GET", `Getting private playlist "${playlistId}" failed for user "${userId}". "${err}"`);
+				logger.error("PLAYLIST_GET", `Getting private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 				return cb({ status: 'failure', message: err});
 			}
 			}
-			logger.success("PLAYLIST_GET", `Successfully got private playlist "${playlistId}" for user "${userId}".`);
+			logger.success("PLAYLIST_GET", `Successfully got private playlist "${playlistId}" for user "${session.userId}".`);
 			cb({
 			cb({
 				status: 'success',
 				status: 'success',
 				data: playlist
 				data: playlist
@@ -211,12 +207,11 @@ let lib = {
 	 * @param {String} playlistId - the id of the playlist we are updating
 	 * @param {String} playlistId - the id of the playlist we are updating
 	 * @param {Object} playlist - the new private playlist object
 	 * @param {Object} playlist - the new private playlist object
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	update: hooks.loginRequired((session, playlistId, playlist, cb, userId) => {
+	update: hooks.loginRequired((session, playlistId, playlist, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				db.models.playlist.updateOne({ _id: playlistId, createdBy: userId }, playlist, {runValidators: true}, next);
+				db.models.playlist.updateOne({ _id: playlistId, createdBy: session.userId }, playlist, {runValidators: true}, next);
 			},
 			},
 
 
 			(res, next) => {
 			(res, next) => {
@@ -225,10 +220,10 @@ let lib = {
 		], async (err, playlist) => {
 		], async (err, playlist) => {
 			if (err) {
 			if (err) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("PLAYLIST_UPDATE", `Updating private playlist "${playlistId}" failed for user "${userId}". "${err}"`);
+				logger.error("PLAYLIST_UPDATE", `Updating private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 				return cb({ status: 'failure', message: err});
 			}
 			}
-			logger.success("PLAYLIST_UPDATE", `Successfully updated private playlist "${playlistId}" for user "${userId}".`);
+			logger.success("PLAYLIST_UPDATE", `Successfully updated private playlist "${playlistId}" for user "${session.userId}".`);
 			cb({
 			cb({
 				status: 'success',
 				status: 'success',
 				data: playlist
 				data: playlist
@@ -243,13 +238,12 @@ let lib = {
 	 * @param {String} songId - the id of the song we are trying to add
 	 * @param {String} songId - the id of the song we are trying to add
 	 * @param {String} playlistId - the id of the playlist we are adding the song to
 	 * @param {String} playlistId - the id of the playlist we are adding the song to
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	addSongToPlaylist: hooks.loginRequired((session, songId, playlistId, cb, userId) => {
+	addSongToPlaylist: hooks.loginRequired((session, songId, playlistId, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				playlists.getPlaylist(playlistId, (err, playlist) => {
 				playlists.getPlaylist(playlistId, (err, playlist) => {
-					if (err || !playlist || playlist.createdBy !== userId) return next('Something went wrong when trying to get the playlist');
+					if (err || !playlist || playlist.createdBy !== session.userId) return next('Something went wrong when trying to get the playlist');
 
 
 					async.each(playlist.songs, (song, next) => {
 					async.each(playlist.songs, (song, next) => {
 						if (song.songId === songId) return next('That song is already in the playlist');
 						if (song.songId === songId) return next('That song is already in the playlist');
@@ -285,11 +279,11 @@ let lib = {
 		async (err, playlist, newSong) => {
 		async (err, playlist, newSong) => {
 			if (err) {
 			if (err) {
 				err = await 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}"`);
+				logger.error("PLAYLIST_ADD_SONG", `Adding song "${songId}" to private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 				return cb({ status: 'failure', message: err});
 			} else {
 			} else {
-				logger.success("PLAYLIST_ADD_SONG", `Successfully added song "${songId}" to private playlist "${playlistId}" for user "${userId}".`);
-				cache.pub('playlist.addSong', { playlistId: playlist._id, song: newSong, userId });
+				logger.success("PLAYLIST_ADD_SONG", `Successfully added song "${songId}" to private playlist "${playlistId}" for user "${session.userId}".`);
+				cache.pub('playlist.addSong', { playlistId: playlist._id, song: newSong, userId: session.userId });
 				return cb({ status: 'success', message: 'Song has been successfully added to the playlist', data: playlist.songs });
 				return cb({ status: 'success', message: 'Song has been successfully added to the playlist', data: playlist.songs });
 			}
 			}
 		});
 		});
@@ -302,9 +296,8 @@ let lib = {
 	 * @param {String} url - the url of the the YouTube playlist
 	 * @param {String} url - the url of the the YouTube playlist
 	 * @param {String} playlistId - the id of the playlist we are adding the set of songs to
 	 * @param {String} playlistId - the id of the playlist we are adding the set of songs to
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	addSetToPlaylist: hooks.loginRequired((session, url, playlistId, cb, userId) => {
+	addSetToPlaylist: hooks.loginRequired((session, url, playlistId, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				utils.getPlaylistFromYouTube(url, songs => {
 				utils.getPlaylistFromYouTube(url, songs => {
@@ -328,16 +321,16 @@ let lib = {
 			},
 			},
 
 
 			(playlist, next) => {
 			(playlist, next) => {
-				if (!playlist || playlist.createdBy !== userId) return next('Playlist not found.');
+				if (!playlist || playlist.createdBy !== session.userId) return next('Playlist not found.');
 				next(null, playlist);
 				next(null, playlist);
 			}
 			}
 		], async (err, playlist) => {
 		], async (err, playlist) => {
 			if (err) {
 			if (err) {
 				err = await 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}"`);
+				logger.error("PLAYLIST_IMPORT", `Importing a YouTube playlist to private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 				return cb({ status: 'failure', message: err});
 			} else {
 			} else {
-				logger.success("PLAYLIST_IMPORT", `Successfully imported a YouTube playlist to private playlist "${playlistId}" for user "${userId}".`);
+				logger.success("PLAYLIST_IMPORT", `Successfully imported a YouTube playlist to private playlist "${playlistId}" for user "${session.userId}".`);
 				cb({ status: 'success', message: 'Playlist has been successfully imported.', data: playlist.songs });
 				cb({ status: 'success', message: 'Playlist has been successfully imported.', data: playlist.songs });
 			}
 			}
 		});
 		});
@@ -350,9 +343,8 @@ let lib = {
 	 * @param {String} songId - the id of the song we are removing from the private playlist
 	 * @param {String} songId - the id of the song we are removing from the private playlist
 	 * @param {String} playlistId - the id of the playlist we are removing the song from
 	 * @param {String} playlistId - the id of the playlist we are removing the song from
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	removeSongFromPlaylist: hooks.loginRequired((session, songId, playlistId, cb, userId) => {
+	removeSongFromPlaylist: hooks.loginRequired((session, songId, playlistId, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				if (!songId || typeof songId !== 'string') return next('Invalid song id.');
 				if (!songId || typeof songId !== 'string') return next('Invalid song id.');
@@ -365,7 +357,7 @@ let lib = {
 			},
 			},
 
 
 			(playlist, next) => {
 			(playlist, next) => {
-				if (!playlist || playlist.createdBy !== userId) return next('Playlist not found');
+				if (!playlist || playlist.createdBy !== session.userId) return next('Playlist not found');
 				db.models.playlist.updateOne({_id: playlistId}, {$pull: {songs: {songId: songId}}}, next);
 				db.models.playlist.updateOne({_id: playlistId}, {$pull: {songs: {songId: songId}}}, next);
 			},
 			},
 
 
@@ -375,11 +367,11 @@ let lib = {
 		], async (err, playlist) => {
 		], async (err, playlist) => {
 			if (err) {
 			if (err) {
 				err = await 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}"`);
+				logger.error("PLAYLIST_REMOVE_SONG", `Removing song "${songId}" from private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 				return cb({ status: 'failure', message: err});
 			} else {
 			} else {
-				logger.success("PLAYLIST_REMOVE_SONG", `Successfully removed song "${songId}" from private playlist "${playlistId}" for user "${userId}".`);
-				cache.pub('playlist.removeSong', { playlistId: playlist._id, songId: songId, userId });
+				logger.success("PLAYLIST_REMOVE_SONG", `Successfully removed song "${songId}" from private playlist "${playlistId}" for user "${session.userId}".`);
+				cache.pub('playlist.removeSong', { playlistId: playlist._id, songId: songId, userId: session.userId });
 				return cb({ status: 'success', message: 'Song has been successfully removed from playlist', data: playlist.songs });
 				return cb({ status: 'success', message: 'Song has been successfully removed from playlist', data: playlist.songs });
 			}
 			}
 		});
 		});
@@ -391,12 +383,11 @@ let lib = {
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {String} playlistId - the id of the playlist we are updating the displayName for
 	 * @param {String} playlistId - the id of the playlist we are updating the displayName for
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	updateDisplayName: hooks.loginRequired((session, playlistId, displayName, cb, userId) => {
+	updateDisplayName: hooks.loginRequired((session, playlistId, displayName, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				db.models.playlist.updateOne({ _id: playlistId, createdBy: userId }, { $set: { displayName } }, {runValidators: true}, next);
+				db.models.playlist.updateOne({ _id: playlistId, createdBy: session.userId }, { $set: { displayName } }, {runValidators: true}, next);
 			},
 			},
 
 
 			(res, next) => {
 			(res, next) => {
@@ -405,11 +396,11 @@ let lib = {
 		], async (err, playlist) => {
 		], async (err, playlist) => {
 			if (err) {
 			if (err) {
 				err = await 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}"`);
+				logger.error("PLAYLIST_UPDATE_DISPLAY_NAME", `Updating display name to "${displayName}" for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 				return cb({ status: 'failure', message: err});
 			}
 			}
-			logger.success("PLAYLIST_UPDATE_DISPLAY_NAME", `Successfully updated display name to "${displayName}" for private playlist "${playlistId}" for user "${userId}".`);
-			cache.pub('playlist.updateDisplayName', {playlistId: playlistId, displayName: displayName, userId: userId});
+			logger.success("PLAYLIST_UPDATE_DISPLAY_NAME", `Successfully updated display name to "${displayName}" for private playlist "${playlistId}" for user "${session.userId}".`);
+			cache.pub('playlist.updateDisplayName', {playlistId: playlistId, displayName: displayName, userId: session.userId});
 			return cb({ status: 'success', message: 'Playlist has been successfully updated' });
 			return cb({ status: 'success', message: 'Playlist has been successfully updated' });
 		});
 		});
 	}),
 	}),
@@ -421,16 +412,15 @@ let lib = {
 	 * @param {String} playlistId - the id of the playlist we are moving the song to the top from
 	 * @param {String} playlistId - the id of the playlist we are moving the song to the top from
 	 * @param {String} songId - the id of the song we are moving to the top of the list
 	 * @param {String} songId - the id of the song we are moving to the top of the list
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	moveSongToTop: hooks.loginRequired((session, playlistId, songId, cb, userId) => {
+	moveSongToTop: hooks.loginRequired((session, playlistId, songId, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				playlists.getPlaylist(playlistId, next);
 				playlists.getPlaylist(playlistId, next);
 			},
 			},
 
 
 			(playlist, next) => {
 			(playlist, next) => {
-				if (!playlist || playlist.createdBy !== userId) return next('Playlist not found');
+				if (!playlist || playlist.createdBy !== session.userId) return next('Playlist not found');
 				async.each(playlist.songs, (song, next) => {
 				async.each(playlist.songs, (song, next) => {
 					if (song.songId === songId) return next(song);
 					if (song.songId === songId) return next(song);
 					next();
 					next();
@@ -464,11 +454,11 @@ let lib = {
 		], async (err, playlist) => {
 		], async (err, playlist) => {
 			if (err) {
 			if (err) {
 				err = await 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}"`);
+				logger.error("PLAYLIST_MOVE_SONG_TO_TOP", `Moving song "${songId}" to the top for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 				return cb({ status: 'failure', message: err});
 			}
 			}
-			logger.success("PLAYLIST_MOVE_SONG_TO_TOP", `Successfully moved song "${songId}" to the top for private playlist "${playlistId}" for user "${userId}".`);
-			cache.pub('playlist.moveSongToTop', {playlistId, songId, userId: userId});
+			logger.success("PLAYLIST_MOVE_SONG_TO_TOP", `Successfully moved song "${songId}" to the top for private playlist "${playlistId}" for user "${session.userId}".`);
+			cache.pub('playlist.moveSongToTop', {playlistId, songId, userId: session.userId});
 			return cb({ status: 'success', message: 'Playlist has been successfully updated' });
 			return cb({ status: 'success', message: 'Playlist has been successfully updated' });
 		});
 		});
 	}),
 	}),
@@ -480,16 +470,15 @@ let lib = {
 	 * @param {String} playlistId - the id of the playlist we are moving the song to the bottom from
 	 * @param {String} playlistId - the id of the playlist we are moving the song to the bottom from
 	 * @param {String} songId - the id of the song we are moving to the bottom of the list
 	 * @param {String} songId - the id of the song we are moving to the bottom of the list
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	moveSongToBottom: hooks.loginRequired((session, playlistId, songId, cb, userId) => {
+	moveSongToBottom: hooks.loginRequired((session, playlistId, songId, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				playlists.getPlaylist(playlistId, next);
 				playlists.getPlaylist(playlistId, next);
 			},
 			},
 
 
 			(playlist, next) => {
 			(playlist, next) => {
-				if (!playlist || playlist.createdBy !== userId) return next('Playlist not found');
+				if (!playlist || playlist.createdBy !== session.userId) return next('Playlist not found');
 				async.each(playlist.songs, (song, next) => {
 				async.each(playlist.songs, (song, next) => {
 					if (song.songId === songId) return next(song);
 					if (song.songId === songId) return next(song);
 					next();
 					next();
@@ -520,11 +509,11 @@ let lib = {
 		], async (err, playlist) => {
 		], async (err, playlist) => {
 			if (err) {
 			if (err) {
 				err = await 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}"`);
+				logger.error("PLAYLIST_MOVE_SONG_TO_BOTTOM", `Moving song "${songId}" to the bottom for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 				return cb({ status: 'failure', message: err});
 			}
 			}
-			logger.success("PLAYLIST_MOVE_SONG_TO_BOTTOM", `Successfully moved song "${songId}" to the bottom for private playlist "${playlistId}" for user "${userId}".`);
-			cache.pub('playlist.moveSongToBottom', {playlistId, songId, userId: userId});
+			logger.success("PLAYLIST_MOVE_SONG_TO_BOTTOM", `Successfully moved song "${songId}" to the bottom for private playlist "${playlistId}" for user "${session.userId}".`);
+			cache.pub('playlist.moveSongToBottom', {playlistId, songId, userId: session.userId});
 			return cb({ status: 'success', message: 'Playlist has been successfully updated' });
 			return cb({ status: 'success', message: 'Playlist has been successfully updated' });
 		});
 		});
 	}),
 	}),
@@ -535,9 +524,8 @@ let lib = {
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {String} playlistId - the id of the playlist we are moving the song to the top from
 	 * @param {String} playlistId - the id of the playlist we are moving the song to the top from
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	remove: hooks.loginRequired((session, playlistId, cb, userId) => {
+	remove: hooks.loginRequired((session, playlistId, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				playlists.deletePlaylist(playlistId, next);
 				playlists.deletePlaylist(playlistId, next);
@@ -545,11 +533,11 @@ let lib = {
 		], async (err) => {
 		], async (err) => {
 			if (err) {
 			if (err) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("PLAYLIST_REMOVE", `Removing private playlist "${playlistId}" failed for user "${userId}". "${err}"`);
+				logger.error("PLAYLIST_REMOVE", `Removing private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 				return cb({ status: 'failure', message: err});
 			}
 			}
-			logger.success("PLAYLIST_REMOVE", `Successfully removed private playlist "${playlistId}" for user "${userId}".`);
-			cache.pub('playlist.delete', {userId: userId, playlistId});
+			logger.success("PLAYLIST_REMOVE", `Successfully removed private playlist "${playlistId}" for user "${session.userId}".`);
+			cache.pub('playlist.delete', {userId: session.userId, playlistId});
 			return cb({ status: 'success', message: 'Playlist successfully removed' });
 			return cb({ status: 'success', message: 'Playlist successfully removed' });
 		});
 		});
 	})
 	})

+ 6 - 7
backend/logic/actions/punishments.js

@@ -52,13 +52,12 @@ module.exports = {
 	 * @param {String} reason - the reason for the ban
 	 * @param {String} reason - the reason for the ban
 	 * @param {String} expiresAt - the time the ban expires
 	 * @param {String} expiresAt - the time the ban expires
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	banIP: hooks.adminRequired((session, value, reason, expiresAt, cb, userId) => {
+	banIP: hooks.adminRequired((session, value, reason, expiresAt, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				if (value === '') return next('You must provide an IP address to ban.');
-				else if (reason === '') return next('You must provide a reason for the ban.');
+				if (!value) return next('You must provide an IP address to ban.');
+				else if (!reason) return next('You must provide a reason for the ban.');
 				else return next();
 				else return next();
 			},
 			},
 
 
@@ -101,15 +100,15 @@ module.exports = {
 			},
 			},
 
 
 			(next) => {
 			(next) => {
-				punishments.addPunishment('banUserIp', value, reason, expiresAt, userId, next)
+				punishments.addPunishment('banUserIp', value, reason, expiresAt, session.userId, next)
 			}
 			}
 		], async (err, punishment) => {
 		], async (err, punishment) => {
 			if (err && err !== true) {
 			if (err && err !== true) {
 				err = await 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}'`);
+				logger.error("BAN_IP", `User ${session.userId} failed to ban IP address ${value} with the reason ${reason}. '${err}'`);
 				cb({ status: 'failure', message: err });
 				cb({ status: 'failure', message: err });
 			}
 			}
-			logger.success("BAN_IP", `User ${userId} has successfully banned IP address ${value} with the reason ${reason}.`);
+			logger.success("BAN_IP", `User ${session.userId} has successfully banned IP address ${value} with the reason ${reason}.`);
 			cache.pub('ip.ban', { ip: value, punishment });
 			cache.pub('ip.ban', { ip: value, punishment });
 			return cb({
 			return cb({
 				status: 'success',
 				status: 'success',

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

@@ -32,30 +32,24 @@ cache.sub('queue.update', songId => {
 let lib = {
 let lib = {
 
 
 	/**
 	/**
-	 * Gets all queuesongs
+	 * Returns the length of the queue songs list
 	 *
 	 *
-	 * @param {Object} session - the session object automatically added by socket.io
-	 * @param {Function} cb - gets called with the result
+	 * @param session
+	 * @param cb
 	 */
 	 */
-	index: hooks.adminRequired((session, cb) => {
+	length: hooks.adminRequired((session, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				db.models.queueSong.find({}, next);
+				db.models.queueSong.countDocuments({}, next);
 			}
 			}
-		], async (err, songs) => {
+		], async (err, count) => {
 			if (err) {
 			if (err) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("QUEUE_INDEX", `Indexing queuesongs failed. "${err}"`);
-				return cb({status: 'failure', message: err});
-			} else {
-				module.exports.getSet(session, 1, result => {
-					logger.success("QUEUE_INDEX", `Indexing queuesongs successful.`);
-					return cb({
-						songs: result,
-						maxLength: songs.length
-					});
-				});
+				logger.error("QUEUE_SONGS_LENGTH", `Failed to get length from queue songs. "${err}"`);
+				return cb({'status': 'failure', 'message': err});
 			}
 			}
+			logger.success("QUEUE_SONGS_LENGTH", `Got length from queue songs successfully.`);
+			cb(count);
 		});
 		});
 	}),
 	}),
 
 
@@ -67,9 +61,18 @@ let lib = {
 	 * @param cb
 	 * @param cb
 	 */
 	 */
 	getSet: hooks.adminRequired((session, set, cb) => {
 	getSet: hooks.adminRequired((session, set, cb) => {
-		db.models.queueSong.find({}).limit(50 * set).exec((err, songs) => {
-			if (err) throw err;
-			cb(songs.splice(Math.max(songs.length - 50, 0)));
+		async.waterfall([
+			(next) => {
+				db.models.queueSong.find({}).skip(15 * (set - 1)).limit(15).exec(next);
+			},
+		], async (err, songs) => {
+			if (err) {
+				err = await utils.getError(err);
+				logger.error("QUEUE_SONGS_GET_SET", `Failed to get set from queue songs. "${err}"`);
+				return cb({'status': 'failure', 'message': err});
+			}
+			logger.success("QUEUE_SONGS_GET_SET", `Got set from queue songs successfully.`);
+			cb(songs);
 		});
 		});
 	}),
 	}),
 
 
@@ -80,9 +83,8 @@ let lib = {
 	 * @param {String} songId - the id of the queuesong that gets updated
 	 * @param {String} songId - the id of the queuesong that gets updated
 	 * @param {Object} updatedSong - the object of the updated queueSong
 	 * @param {Object} updatedSong - the object of the updated queueSong
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	update: hooks.adminRequired((session, songId, updatedSong, cb, userId) => {
+	update: hooks.adminRequired((session, songId, updatedSong, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				db.models.queueSong.findOne({_id: songId}, next);
 				db.models.queueSong.findOne({_id: songId}, next);
@@ -99,11 +101,11 @@ let lib = {
 		], async (err) => {
 		], async (err) => {
 			if (err) {
 			if (err) {
 				err = await  utils.getError(err);
 				err = await  utils.getError(err);
-				logger.error("QUEUE_UPDATE", `Updating queuesong "${songId}" failed for user ${userId}. "${err}"`);
+				logger.error("QUEUE_UPDATE", `Updating queuesong "${songId}" failed for user ${session.userId}. "${err}"`);
 				return cb({status: 'failure', message: err});
 				return cb({status: 'failure', message: err});
 			}
 			}
 			cache.pub('queue.update', songId);
 			cache.pub('queue.update', songId);
-			logger.success("QUEUE_UPDATE", `User "${userId}" successfully update queuesong "${songId}".`);
+			logger.success("QUEUE_UPDATE", `User "${session.userId}" successfully update queuesong "${songId}".`);
 			return cb({status: 'success', message: 'Successfully updated song.'});
 			return cb({status: 'success', message: 'Successfully updated song.'});
 		});
 		});
 	}),
 	}),
@@ -114,7 +116,6 @@ let lib = {
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {String} songId - the id of the queuesong that gets removed
 	 * @param {String} songId - the id of the queuesong that gets removed
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
 	remove: hooks.adminRequired((session, songId, cb, userId) => {
 	remove: hooks.adminRequired((session, songId, cb, userId) => {
 		async.waterfall([
 		async.waterfall([
@@ -124,11 +125,11 @@ let lib = {
 		], async (err) => {
 		], async (err) => {
 			if (err) {
 			if (err) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("QUEUE_REMOVE", `Removing queuesong "${songId}" failed for user ${userId}. "${err}"`);
+				logger.error("QUEUE_REMOVE", `Removing queuesong "${songId}" failed for user ${session.userId}. "${err}"`);
 				return cb({status: 'failure', message: err});
 				return cb({status: 'failure', message: err});
 			}
 			}
 			cache.pub('queue.removedSong', songId);
 			cache.pub('queue.removedSong', songId);
-			logger.success("QUEUE_REMOVE", `User "${userId}" successfully removed queuesong "${songId}".`);
+			logger.success("QUEUE_REMOVE", `User "${session.userId}" successfully removed queuesong "${songId}".`);
 			return cb({status: 'success', message: 'Successfully updated song.'});
 			return cb({status: 'success', message: 'Successfully updated song.'});
 		});
 		});
 	}),
 	}),
@@ -139,9 +140,8 @@ let lib = {
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {String} songId - the id of the song that gets added
 	 * @param {String} songId - the id of the song that gets added
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	add: hooks.loginRequired((session, songId, cb, userId) => {
+	add: hooks.loginRequired((session, songId, cb) => {
 		let requestedAt = Date.now();
 		let requestedAt = Date.now();
 
 
 		async.waterfall([
 		async.waterfall([
@@ -159,12 +159,13 @@ let lib = {
 				if (song) return next('This song has already been added.');
 				if (song) return next('This song has already been added.');
 				//TODO Add err object as first param of callback
 				//TODO Add err object as first param of callback
 				utils.getSongFromYouTube(songId, (song) => {
 				utils.getSongFromYouTube(songId, (song) => {
+					song.duration = -1;
 					song.artists = [];
 					song.artists = [];
 					song.genres = [];
 					song.genres = [];
 					song.skipDuration = 0;
 					song.skipDuration = 0;
 					song.thumbnail = `${config.get("domain")}/assets/notes.png`;
 					song.thumbnail = `${config.get("domain")}/assets/notes.png`;
 					song.explicit = false;
 					song.explicit = false;
-					song.requestedBy = userId;
+					song.requestedBy = session.userId;
 					song.requestedAt = requestedAt;
 					song.requestedAt = requestedAt;
 					next(null, song);
 					next(null, song);
 				});
 				});
@@ -177,13 +178,13 @@ let lib = {
 			},*/
 			},*/
 			(newSong, next) => {
 			(newSong, next) => {
 				const song = new db.models.queueSong(newSong);
 				const song = new db.models.queueSong(newSong);
-				song.save((err, song) => {
+				song.save({ validateBeforeSave: false }, (err, song) => {
 					if (err) return next(err);
 					if (err) return next(err);
 					next(null, song);
 					next(null, song);
 				});
 				});
 			},
 			},
 			(newSong, next) => {
 			(newSong, next) => {
-				db.models.user.findOne({ _id: userId }, (err, user) => {
+				db.models.user.findOne({ _id: session.userId }, (err, user) => {
 					if (err) next(err, newSong);
 					if (err) next(err, newSong);
 					else {
 					else {
 						user.statistics.songsRequested = user.statistics.songsRequested + 1;
 						user.statistics.songsRequested = user.statistics.songsRequested + 1;
@@ -197,11 +198,11 @@ let lib = {
 		], async (err, newSong) => {
 		], async (err, newSong) => {
 			if (err) {
 			if (err) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("QUEUE_ADD", `Adding queuesong "${songId}" failed for user ${userId}. "${err}"`);
+				logger.error("QUEUE_ADD", `Adding queuesong "${songId}" failed for user ${session.userId}. "${err}"`);
 				return cb({status: 'failure', message: err});
 				return cb({status: 'failure', message: err});
 			}
 			}
 			cache.pub('queue.newSong', newSong._id);
 			cache.pub('queue.newSong', newSong._id);
-			logger.success("QUEUE_ADD", `User "${userId}" successfully added queuesong "${songId}".`);
+			logger.success("QUEUE_ADD", `User "${session.userId}" successfully added queuesong "${songId}".`);
 			return cb({ status: 'success', message: 'Successfully added that song to the queue' });
 			return cb({ status: 'success', message: 'Successfully added that song to the queue' });
 		});
 		});
 	}),
 	}),
@@ -212,9 +213,8 @@ let lib = {
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {String} url - the url of the the YouTube playlist
 	 * @param {String} url - the url of the the YouTube playlist
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	addSetToQueue: hooks.loginRequired((session, url, cb, userId) => {
+	addSetToQueue: hooks.loginRequired((session, url, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				utils.getPlaylistFromYouTube(url, songs => {
 				utils.getPlaylistFromYouTube(url, songs => {
@@ -236,10 +236,10 @@ let lib = {
 		], async (err) => {
 		], async (err) => {
 			if (err) {
 			if (err) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("QUEUE_IMPORT", `Importing a YouTube playlist to the queue failed for user "${userId}". "${err}"`);
+				logger.error("QUEUE_IMPORT", `Importing a YouTube playlist to the queue failed for user "${session.userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 				return cb({ status: 'failure', message: err});
 			} else {
 			} else {
-				logger.success("QUEUE_IMPORT", `Successfully imported a YouTube playlist to the queue for user "${userId}".`);
+				logger.success("QUEUE_IMPORT", `Successfully imported a YouTube playlist to the queue for user "${session.userId}".`);
 				cb({ status: 'success', message: 'Playlist has been successfully imported.' });
 				cb({ status: 'success', message: 'Playlist has been successfully imported.' });
 			}
 			}
 		});
 		});

+ 7 - 9
backend/logic/actions/reports.js

@@ -147,9 +147,8 @@ module.exports = {
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {String} reportId - the id of the report that is getting resolved
 	 * @param {String} reportId - the id of the report that is getting resolved
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	resolve: hooks.adminRequired((session, reportId, cb, userId) => {
+	resolve: hooks.adminRequired((session, reportId, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				db.models.report.findOne({ _id: reportId }).exec(next);
 				db.models.report.findOne({ _id: reportId }).exec(next);
@@ -166,11 +165,11 @@ module.exports = {
 		], async (err) => {
 		], async (err) => {
 			if (err) {
 			if (err) {
 				err = await  utils.getError(err);
 				err = await  utils.getError(err);
-				logger.error("REPORTS_RESOLVE", `Resolving report "${reportId}" failed by user "${userId}". "${err}"`);
+				logger.error("REPORTS_RESOLVE", `Resolving report "${reportId}" failed by user "${session.userId}". "${err}"`);
 				return cb({ 'status': 'failure', 'message': err});
 				return cb({ 'status': 'failure', 'message': err});
 			} else {
 			} else {
 				cache.pub('report.resolve', reportId);
 				cache.pub('report.resolve', reportId);
-				logger.success("REPORTS_RESOLVE", `User "${userId}" resolved report "${reportId}".`);
+				logger.success("REPORTS_RESOLVE", `User "${session.userId}" resolved report "${reportId}".`);
 				cb({ status: 'success', message: 'Successfully resolved Report' });
 				cb({ status: 'success', message: 'Successfully resolved Report' });
 			}
 			}
 		});
 		});
@@ -182,9 +181,8 @@ module.exports = {
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Object} data - the object of the report data
 	 * @param {Object} data - the object of the report data
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	create: hooks.loginRequired((session, data, cb, userId) => {
+	create: hooks.loginRequired((session, data, cb) => {
 		async.waterfall([
 		async.waterfall([
 
 
 			(next) => {
 			(next) => {
@@ -231,7 +229,7 @@ module.exports = {
 			},
 			},
 
 
 			(next) => {
 			(next) => {
-				data.createdBy = userId;
+				data.createdBy = session.userId;
 				data.createdAt = Date.now();
 				data.createdAt = Date.now();
 				db.models.report.create(data, next);
 				db.models.report.create(data, next);
 			}
 			}
@@ -239,11 +237,11 @@ module.exports = {
 		], async (err, report) => {
 		], async (err, report) => {
 			if (err) {
 			if (err) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("REPORTS_CREATE", `Creating report for "${data.song._id}" failed by user "${userId}". "${err}"`);
+				logger.error("REPORTS_CREATE", `Creating report for "${data.song._id}" failed by user "${session.userId}". "${err}"`);
 				return cb({ 'status': 'failure', 'message': err });
 				return cb({ 'status': 'failure', 'message': err });
 			} else {
 			} else {
 				cache.pub('report.create', report);
 				cache.pub('report.create', report);
-				logger.success("REPORTS_CREATE", `User "${userId}" created report for "${data.songId}".`);
+				logger.success("REPORTS_CREATE", `User "${session.userId}" created report for "${data.songId}".`);
 				return cb({ 'status': 'success', 'message': 'Successfully created report' });
 				return cb({ 'status': 'success', 'message': 'Successfully created report' });
 			}
 			}
 		});
 		});

+ 26 - 34
backend/logic/actions/songs.js

@@ -99,8 +99,8 @@ module.exports = {
 	getSet: hooks.adminRequired((session, set, cb) => {
 	getSet: hooks.adminRequired((session, set, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				db.models.song.find({}).limit(15 * set).exec(next);
-			}
+				db.models.song.find({}).skip(15 * (set - 1)).limit(15).exec(next);
+			},
 		], async (err, songs) => {
 		], async (err, songs) => {
 			if (err) {
 			if (err) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
@@ -108,9 +108,7 @@ module.exports = {
 				return cb({'status': 'failure', 'message': err});
 				return cb({'status': 'failure', 'message': err});
 			}
 			}
 			logger.success("SONGS_GET_SET", `Got set from songs successfully.`);
 			logger.success("SONGS_GET_SET", `Got set from songs successfully.`);
-			logger.stationIssue(songs.length, true);
-			logger.stationIssue(Math.max(songs.length - 15, 0), true);
-			cb(songs.splice(Math.max(songs.length - 15, 0)));
+			cb(songs);
 		});
 		});
 	}),
 	}),
 
 
@@ -203,9 +201,8 @@ module.exports = {
 	 * @param session
 	 * @param session
 	 * @param song - the song object
 	 * @param song - the song object
 	 * @param cb
 	 * @param cb
-	 * @param userId
 	 */
 	 */
-	add: hooks.adminRequired((session, song, cb, userId) => {
+	add: hooks.adminRequired((session, song, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				db.models.song.findOne({songId: song.songId}, next);
 				db.models.song.findOne({songId: song.songId}, next);
@@ -218,7 +215,7 @@ module.exports = {
 
 
 			(next) => {
 			(next) => {
 				const newSong = new db.models.song(song);
 				const newSong = new db.models.song(song);
-				newSong.acceptedBy = userId;
+				newSong.acceptedBy = session.userId;
 				newSong.acceptedAt = Date.now();
 				newSong.acceptedAt = Date.now();
 				newSong.save(next);
 				newSong.save(next);
 			},
 			},
@@ -231,10 +228,10 @@ module.exports = {
 		], async (err) => {
 		], async (err) => {
 			if (err) {
 			if (err) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("SONGS_ADD", `User "${userId}" failed to add song. "${err}"`);
+				logger.error("SONGS_ADD", `User "${session.userId}" failed to add song. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 				return cb({'status': 'failure', 'message': err});
 			}
 			}
-			logger.success("SONGS_ADD", `User "${userId}" successfully added song "${song.songId}".`);
+			logger.success("SONGS_ADD", `User "${session.userId}" successfully added song "${song.songId}".`);
 			cache.pub('song.added', song.songId);
 			cache.pub('song.added', song.songId);
 			cb({status: 'success', message: 'Song has been moved from the queue successfully.'});
 			cb({status: 'success', message: 'Song has been moved from the queue successfully.'});
 		});
 		});
@@ -247,9 +244,8 @@ module.exports = {
 	 * @param session
 	 * @param session
 	 * @param songId - the song id
 	 * @param songId - the song id
 	 * @param cb
 	 * @param cb
-	 * @param userId
 	 */
 	 */
-	like: hooks.loginRequired((session, songId, cb, userId) => {
+	like: hooks.loginRequired((session, songId, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				db.models.song.findOne({songId}, next);
 				db.models.song.findOne({songId}, next);
@@ -262,14 +258,14 @@ module.exports = {
 		], async (err, song) => {
 		], async (err, song) => {
 			if (err) {
 			if (err) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("SONGS_LIKE", `User "${userId}" failed to like song ${songId}. "${err}"`);
+				logger.error("SONGS_LIKE", `User "${session.userId}" failed to like song ${songId}. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 				return cb({'status': 'failure', 'message': err});
 			}
 			}
 			let oldSongId = songId;
 			let oldSongId = songId;
 			songId = song._id;
 			songId = song._id;
-			db.models.user.findOne({ _id: userId }, (err, user) => {
+			db.models.user.findOne({ _id: session.userId }, (err, user) => {
 				if (user.liked.indexOf(songId) !== -1) return cb({ status: 'failure', message: 'You have already liked this song.' });
 				if (user.liked.indexOf(songId) !== -1) return cb({ status: 'failure', message: 'You have already liked this song.' });
-				db.models.user.updateOne({_id: userId}, {$push: {liked: songId}, $pull: {disliked: songId}}, err => {
+				db.models.user.updateOne({_id: session.userId}, {$push: {liked: songId}, $pull: {disliked: songId}}, err => {
 					if (!err) {
 					if (!err) {
 						db.models.user.countDocuments({"liked": songId}, (err, likes) => {
 						db.models.user.countDocuments({"liked": songId}, (err, likes) => {
 							if (err) return cb({ status: 'failure', message: 'Something went wrong while liking this song.' });
 							if (err) return cb({ status: 'failure', message: 'Something went wrong while liking this song.' });
@@ -295,9 +291,8 @@ module.exports = {
 	 * @param session
 	 * @param session
 	 * @param songId - the song id
 	 * @param songId - the song id
 	 * @param cb
 	 * @param cb
-	 * @param userId
 	 */
 	 */
-	dislike: hooks.loginRequired((session, songId, cb, userId) => {
+	dislike: hooks.loginRequired((session, songId, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				db.models.song.findOne({songId}, next);
 				db.models.song.findOne({songId}, next);
@@ -310,14 +305,14 @@ module.exports = {
 		], async (err, song) => {
 		], async (err, song) => {
 			if (err) {
 			if (err) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("SONGS_DISLIKE", `User "${userId}" failed to like song ${songId}. "${err}"`);
+				logger.error("SONGS_DISLIKE", `User "${session.userId}" failed to like song ${songId}. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 				return cb({'status': 'failure', 'message': err});
 			}
 			}
 			let oldSongId = songId;
 			let oldSongId = songId;
 			songId = song._id;
 			songId = song._id;
-			db.models.user.findOne({ _id: userId }, (err, user) => {
+			db.models.user.findOne({ _id: session.userId }, (err, user) => {
 				if (user.disliked.indexOf(songId) !== -1) return cb({ status: 'failure', message: 'You have already disliked this song.' });
 				if (user.disliked.indexOf(songId) !== -1) return cb({ status: 'failure', message: 'You have already disliked this song.' });
-				db.models.user.updateOne({_id: userId}, {$push: {disliked: songId}, $pull: {liked: songId}}, err => {
+				db.models.user.updateOne({_id: session.userId}, {$push: {disliked: songId}, $pull: {liked: songId}}, err => {
 					if (!err) {
 					if (!err) {
 						db.models.user.countDocuments({"liked": songId}, (err, likes) => {
 						db.models.user.countDocuments({"liked": songId}, (err, likes) => {
 							if (err) return cb({ status: 'failure', message: 'Something went wrong while disliking this song.' });
 							if (err) return cb({ status: 'failure', message: 'Something went wrong while disliking this song.' });
@@ -343,9 +338,8 @@ module.exports = {
 	 * @param session
 	 * @param session
 	 * @param songId - the song id
 	 * @param songId - the song id
 	 * @param cb
 	 * @param cb
-	 * @param userId
 	 */
 	 */
-	undislike: hooks.loginRequired((session, songId, cb, userId) => {
+	undislike: hooks.loginRequired((session, songId, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				db.models.song.findOne({songId}, next);
 				db.models.song.findOne({songId}, next);
@@ -358,17 +352,17 @@ module.exports = {
 		], async (err, song) => {
 		], async (err, song) => {
 			if (err) {
 			if (err) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("SONGS_UNDISLIKE", `User "${userId}" failed to like song ${songId}. "${err}"`);
+				logger.error("SONGS_UNDISLIKE", `User "${session.userId}" failed to like song ${songId}. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 				return cb({'status': 'failure', 'message': err});
 			}
 			}
 			let oldSongId = songId;
 			let oldSongId = songId;
 			songId = song._id;
 			songId = song._id;
-			db.models.user.findOne({_id: userId}, (err, user) => {
+			db.models.user.findOne({_id: session.userId}, (err, user) => {
 				if (user.disliked.indexOf(songId) === -1) return cb({
 				if (user.disliked.indexOf(songId) === -1) return cb({
 					status: 'failure',
 					status: 'failure',
 					message: 'You have not disliked this song.'
 					message: 'You have not disliked this song.'
 				});
 				});
-				db.models.user.updateOne({_id: userId}, {$pull: {liked: songId, disliked: songId}}, err => {
+				db.models.user.updateOne({_id: session.userId}, {$pull: {liked: songId, disliked: songId}}, err => {
 					if (!err) {
 					if (!err) {
 						db.models.user.countDocuments({"liked": songId}, (err, likes) => {
 						db.models.user.countDocuments({"liked": songId}, (err, likes) => {
 							if (err) return cb({
 							if (err) return cb({
@@ -412,9 +406,8 @@ module.exports = {
 	 * @param session
 	 * @param session
 	 * @param songId - the song id
 	 * @param songId - the song id
 	 * @param cb
 	 * @param cb
-	 * @param userId
 	 */
 	 */
-	unlike: hooks.loginRequired((session, songId, cb, userId) => {
+	unlike: hooks.loginRequired((session, songId, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				db.models.song.findOne({songId}, next);
 				db.models.song.findOne({songId}, next);
@@ -427,14 +420,14 @@ module.exports = {
 		], async (err, song) => {
 		], async (err, song) => {
 			if (err) {
 			if (err) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("SONGS_UNLIKE", `User "${userId}" failed to like song ${songId}. "${err}"`);
+				logger.error("SONGS_UNLIKE", `User "${session.userId}" failed to like song ${songId}. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 				return cb({'status': 'failure', 'message': err});
 			}
 			}
 			let oldSongId = songId;
 			let oldSongId = songId;
 			songId = song._id;
 			songId = song._id;
-			db.models.user.findOne({ _id: userId }, (err, user) => {
+			db.models.user.findOne({ _id: session.userId }, (err, user) => {
 				if (user.liked.indexOf(songId) === -1) return cb({ status: 'failure', message: 'You have not liked this song.' });
 				if (user.liked.indexOf(songId) === -1) return cb({ status: 'failure', message: 'You have not liked this song.' });
-				db.models.user.updateOne({_id: userId}, {$pull: {liked: songId, disliked: songId}}, err => {
+				db.models.user.updateOne({_id: session.userId}, {$pull: {liked: songId, disliked: songId}}, err => {
 					if (!err) {
 					if (!err) {
 						db.models.user.countDocuments({"liked": songId}, (err, likes) => {
 						db.models.user.countDocuments({"liked": songId}, (err, likes) => {
 							if (err) return cb({ status: 'failure', message: 'Something went wrong while unliking this song.' });
 							if (err) return cb({ status: 'failure', message: 'Something went wrong while unliking this song.' });
@@ -460,9 +453,8 @@ module.exports = {
 	 * @param session
 	 * @param session
 	 * @param songId - the song id
 	 * @param songId - the song id
 	 * @param cb
 	 * @param cb
-	 * @param userId
 	 */
 	 */
-	getOwnSongRatings: hooks.loginRequired((session, songId, cb, userId) => {
+	getOwnSongRatings: hooks.loginRequired((session, songId, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				db.models.song.findOne({songId}, next);
 				db.models.song.findOne({songId}, next);
@@ -475,11 +467,11 @@ module.exports = {
 		], async (err, song) => {
 		], async (err, song) => {
 			if (err) {
 			if (err) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("SONGS_GET_OWN_RATINGS", `User "${userId}" failed to get ratings for ${songId}. "${err}"`);
+				logger.error("SONGS_GET_OWN_RATINGS", `User "${session.userId}" failed to get ratings for ${songId}. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 				return cb({'status': 'failure', 'message': err});
 			}
 			}
 			let newSongId = song._id;
 			let newSongId = song._id;
-			db.models.user.findOne({_id: userId}, (err, user) => {
+			db.models.user.findOne({_id: session.userId}, (err, user) => {
 				if (!err && user) {
 				if (!err && user) {
 					return cb({
 					return cb({
 						status: 'success',
 						status: 'success',

+ 62 - 101
backend/logic/actions/stations.js

@@ -214,31 +214,18 @@ module.exports = {
 				next(null, stations);
 				next(null, stations);
 			},
 			},
 
 
-			(stations, next) => {
+			(stationsArray, next) => {
 				let resultStations = [];
 				let resultStations = [];
-				async.each(stations, (station, next) => {
+				async.each(stationsArray, (station, next) => {
 					async.waterfall([
 					async.waterfall([
 						(next) => {
 						(next) => {
-							if (station.privacy === 'public') return next(true);
-							if (!session.sessionId) return next(`Insufficient permissions.`);
-							cache.hget('sessions', session.sessionId, next);
-						},
-
-						(session, next) => {
-							if (!session) return next(`Insufficient permissions.`);
-							db.models.user.findOne({_id: session.userId}, next);
-						},
-
-						(user, next) => {
-							if (!user) return next(`Insufficient permissions.`);
-							if (user.role === 'admin') return next(true);
-							if (station.type === 'official') return next(`Insufficient permissions.`);
-							if (station.owner === session.userId) return next(true);
-							next(`Insufficient permissions.`);
+							stations.canUserViewStation(station, session.userId, (err, exists) => {
+								next(err, exists);
+							});
 						}
 						}
-					], (err) => {
+					], (err, exists) => {
 						station.userCount = usersPerStationCount[station._id] || 0;
 						station.userCount = usersPerStationCount[station._id] || 0;
-						if (err === true) resultStations.push(station);
+						if (exists) resultStations.push(station);
 						next();
 						next();
 					});
 					});
 				}, () => {
 				}, () => {
@@ -257,30 +244,32 @@ module.exports = {
 	},
 	},
 
 
 	/**
 	/**
-	 * Finds a station by name
+	 * Verifies that a station exists
 	 *
 	 *
 	 * @param session
 	 * @param session
 	 * @param stationName - the station name
 	 * @param stationName - the station name
 	 * @param cb
 	 * @param cb
 	 */
 	 */
-	findByName: (session, stationName, cb) => {
+	existsByName: (session, stationName, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				stations.getStationByName(stationName, next);
 				stations.getStationByName(stationName, next);
 			},
 			},
 
 
 			(station, next) => {
 			(station, next) => {
-				if (!station) return next('Station not found.');
-				next(null, station);
+				if (!station) return next(null, false);
+				stations.canUserViewStation(station, session.userId, (err, exists) => {
+					next(err, exists);
+				});
 			}
 			}
-		], async (err, station) => {
+		], async (err, exists) => {
 			if (err) {
 			if (err) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("STATIONS_FIND_BY_NAME", `Finding station "${stationName}" failed. "${err}"`);
+				logger.error("STATION_EXISTS_BY_NAME", `Checking if station "${stationName}" exists failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 				return cb({'status': 'failure', 'message': err});
 			}
 			}
-			logger.success("STATIONS_FIND_BY_NAME", `Found station "${stationName}" successfully.`, false);
-			cb({status: 'success', data: station});
+			logger.success("STATION_EXISTS_BY_NAME", `Station "${stationName}" exists successfully.`/*, false*/);
+			cb({status: 'success', exists});
 		});
 		});
 	},
 	},
 
 
@@ -297,6 +286,14 @@ module.exports = {
 				stations.getStation(stationId, next);
 				stations.getStation(stationId, next);
 			},
 			},
 
 
+			(station, next) => {
+				stations.canUserViewStation(station, session.userId, (err, canView) => {
+					if (err) return next(err);
+					if (canView) return next(null, station);
+					return next('Insufficient permissions.');
+				});
+			},
+
 			(station, next) => {
 			(station, next) => {
 				if (!station) return next('Station not found.');
 				if (!station) return next('Station not found.');
 				else if (station.type !== 'official') return next('This is not an official station.');
 				else if (station.type !== 'official') return next('This is not an official station.');
@@ -339,27 +336,10 @@ module.exports = {
 
 
 			(station, next) => {
 			(station, next) => {
 				if (!station) return next('Station not found.');
 				if (!station) return next('Station not found.');
-				async.waterfall([
-					(next) => {
-						if (station.privacy !== 'private') return next(true);
-						if (!session.userId) return next('An error occurred while joining the station.');
-						next();
-					},
-
-					(next) => {
-						db.models.user.findOne({_id: session.userId}, next);
-					},
-
-					(user, next) => {
-						if (!user) return next('An error occurred while joining the station.');
-						if (user.role === 'admin') return next(true);
-						if (station.type === 'official') return next('An error occurred while joining the station.');
-						if (station.owner === session.userId) return next(true);
-						next('An error occurred while joining the station.');
-					}
-				], async (err) => {
-					if (err === true) return next(null, station);
-					next(await utils.getError(err));
+				stations.canUserViewStation(station, session.userId, (err, canView) => {
+					if (err) return next(err);
+					if (!canView) next("Not allowed to join station.");
+					else next(null, station);
 				});
 				});
 			},
 			},
 
 
@@ -452,9 +432,8 @@ module.exports = {
 	 * @param session
 	 * @param session
 	 * @param stationId - the station id
 	 * @param stationId - the station id
 	 * @param cb
 	 * @param cb
-	 * @param userId
 	 */
 	 */
-	voteSkip: hooks.loginRequired((session, stationId, cb, userId) => {
+	voteSkip: hooks.loginRequired((session, stationId, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				stations.getStation(stationId, next);
 				stations.getStation(stationId, next);
@@ -462,20 +441,21 @@ module.exports = {
 
 
 			(station, next) => {
 			(station, next) => {
 				if (!station) return next('Station not found.');
 				if (!station) return next('Station not found.');
-				utils.canUserBeInStation(station, userId, (canBe) => {
-					if (canBe) return next(null, station);
+				stations.canUserViewStation(station, session.userId, (err, canView) => {
+					if (err) return next(err);
+					if (canView) return next(null, station);
 					return next('Insufficient permissions.');
 					return next('Insufficient permissions.');
 				});
 				});
 			},
 			},
 
 
 			(station, next) => {
 			(station, next) => {
 				if (!station.currentSong) return next('There is currently no song to skip.');
 				if (!station.currentSong) return next('There is currently no song to skip.');
-				if (station.currentSong.skipVotes.indexOf(userId) !== -1) return next('You have already voted to skip this song.');
+				if (station.currentSong.skipVotes.indexOf(session.userId) !== -1) return next('You have already voted to skip this song.');
 				next(null, station);
 				next(null, station);
 			},
 			},
 
 
 			(station, next) => {
 			(station, next) => {
-				db.models.station.updateOne({_id: stationId}, {$push: {"currentSong.skipVotes": userId}}, next)
+				db.models.station.updateOne({_id: stationId}, {$push: {"currentSong.skipVotes": session.userId}}, next)
 			},
 			},
 
 
 			(res, next) => {
 			(res, next) => {
@@ -580,10 +560,10 @@ module.exports = {
 		], async (err) => {
 		], async (err) => {
 			if (err) {
 			if (err) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("STATIONS_UPDATE_DISPLAY_NAME", `Updating station "${stationId}" displayName to "${newName}" failed. "${err}"`);
+				logger.error("STATIONS_UPDATE_NAME", `Updating station "${stationId}" name to "${newName}" failed. "${err}"`);
 				return cb({'status': 'failure', 'message': err});
 				return cb({'status': 'failure', 'message': err});
 			}
 			}
-			logger.success("STATIONS_UPDATE_DISPLAY_NAME", `Updated station "${stationId}" displayName to "${newName}" successfully.`);
+			logger.success("STATIONS_UPDATE_NAME", `Updated station "${stationId}" name to "${newName}" successfully.`);
 			return cb({'status': 'success', 'message': 'Successfully updated the name.'});
 			return cb({'status': 'success', 'message': 'Successfully updated the name.'});
 		});
 		});
 	}),
 	}),
@@ -868,9 +848,8 @@ module.exports = {
 	 * @param session
 	 * @param session
 	 * @param data - the station data
 	 * @param data - the station data
 	 * @param cb
 	 * @param cb
-	 * @param userId
 	 */
 	 */
-	create: hooks.loginRequired((session, data, cb, userId) => {
+	create: hooks.loginRequired((session, data, cb) => {
 		data.name = data.name.toLowerCase();
 		data.name = data.name.toLowerCase();
 		let blacklist = ["country", "edm", "musare", "hip-hop", "rap", "top-hits", "todays-hits", "old-school", "christmas", "about", "support", "staff", "help", "news", "terms", "privacy", "profile", "c", "community", "tos", "login", "register", "p", "official", "o", "trap", "faq", "team", "donate", "buy", "shop", "forums", "explore", "settings", "admin", "auth", "reset_password"];
 		let blacklist = ["country", "edm", "musare", "hip-hop", "rap", "top-hits", "todays-hits", "old-school", "christmas", "about", "support", "staff", "help", "news", "terms", "privacy", "profile", "c", "community", "tos", "login", "register", "p", "official", "o", "trap", "faq", "team", "donate", "buy", "shop", "forums", "explore", "settings", "admin", "auth", "reset_password"];
 		async.waterfall([
 		async.waterfall([
@@ -887,7 +866,7 @@ module.exports = {
 				if (station) return next('A station with that name or display name already exists.');
 				if (station) return next('A station with that name or display name already exists.');
 				const { name, displayName, description, genres, playlist, type, blacklistedGenres } = data;
 				const { name, displayName, description, genres, playlist, type, blacklistedGenres } = data;
 				if (type === 'official') {
 				if (type === 'official') {
-					db.models.user.findOne({_id: userId}, (err, user) => {
+					db.models.user.findOne({_id: session.userId}, (err, user) => {
 						if (err) return next(err);
 						if (err) return next(err);
 						if (!user) return next('User not found.');
 						if (!user) return next('User not found.');
 						if (user.role !== 'admin') return next('Admin required.');
 						if (user.role !== 'admin') return next('Admin required.');
@@ -911,7 +890,7 @@ module.exports = {
 						description,
 						description,
 						type,
 						type,
 						privacy: 'private',
 						privacy: 'private',
-						owner: userId,
+						owner: session.userId,
 						queue: [],
 						queue: [],
 						currentSong: null
 						currentSong: null
 					}, next);
 					}, next);
@@ -936,9 +915,8 @@ module.exports = {
 	 * @param stationId - the station id
 	 * @param stationId - the station id
 	 * @param songId - the song id
 	 * @param songId - the song id
 	 * @param cb
 	 * @param cb
-	 * @param userId
 	 */
 	 */
-	addToQueue: hooks.loginRequired((session, stationId, songId, cb, userId) => {
+	addToQueue: hooks.loginRequired((session, stationId, songId, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				stations.getStation(stationId, next);
 				stations.getStation(stationId, next);
@@ -947,8 +925,8 @@ module.exports = {
 			(station, next) => {
 			(station, next) => {
 				if (!station) return next('Station not found.');
 				if (!station) return next('Station not found.');
 				if (station.locked) {
 				if (station.locked) {
-					db.models.user.findOne({ _id: userId }, (err, user) => {
-						if (user.role !== 'admin' && station.owner !== userId) return next('Only owners and admins can add songs to a locked queue.');
+					db.models.user.findOne({ _id: session.userId }, (err, user) => {
+						if (user.role !== 'admin' && station.owner !== session.userId) return next('Only owners and admins can add songs to a locked queue.');
 						else return next(null, station);
 						else return next(null, station);
 					});
 					});
 				} else {
 				} else {
@@ -958,8 +936,9 @@ module.exports = {
 
 
 			(station, next) => {
 			(station, next) => {
 				if (station.type !== 'community') return next('That station is not a community station.');
 				if (station.type !== 'community') return next('That station is not a community station.');
-				utils.canUserBeInStation(station, userId, (canBe) => {
-					if (canBe) return next(null, station);
+				stations.canUserViewStation(station, session.userId, (err, canView) => {
+					if (err) return next(err);
+					if (canView) return next(null, station);
 					return next('Insufficient permissions.');
 					return next('Insufficient permissions.');
 				});
 				});
 			},
 			},
@@ -991,7 +970,7 @@ module.exports = {
 
 
 			(song, station, next) => {
 			(song, station, next) => {
 				let queue = station.queue;
 				let queue = station.queue;
-				song.requestedBy = userId;
+				song.requestedBy = session.userId;
 				queue.push(song);
 				queue.push(song);
 
 
 				let totalDuration = 0;
 				let totalDuration = 0;
@@ -1060,9 +1039,8 @@ module.exports = {
 	 * @param stationId - the station id
 	 * @param stationId - the station id
 	 * @param songId - the song id
 	 * @param songId - the song id
 	 * @param cb
 	 * @param cb
-	 * @param userId
 	 */
 	 */
-	removeFromQueue: hooks.ownerRequired((session, stationId, songId, cb, userId) => {
+	removeFromQueue: hooks.ownerRequired((session, stationId, songId, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				if (!songId) return next('Invalid song id.');
 				if (!songId) return next('Invalid song id.');
@@ -1120,8 +1098,9 @@ module.exports = {
 			},
 			},
 
 
 			(station, next) => {
 			(station, next) => {
-				utils.canUserBeInStation(station, session.userId, (canBe) => {
-					if (canBe) return next(null, station);
+				stations.canUserViewStation(station, session.userId, (err, canView) => {
+					if (err) return next(err);
+					if (canView) return next(null, station);
 					return next('Insufficient permissions.');
 					return next('Insufficient permissions.');
 				});
 				});
 			}
 			}
@@ -1143,9 +1122,8 @@ module.exports = {
 	 * @param stationId - the station id
 	 * @param stationId - the station id
 	 * @param playlistId - the private playlist id
 	 * @param playlistId - the private playlist id
 	 * @param cb
 	 * @param cb
-	 * @param userId
 	 */
 	 */
-	selectPrivatePlaylist: hooks.ownerRequired((session, stationId, playlistId, cb, userId) => {
+	selectPrivatePlaylist: hooks.ownerRequired((session, stationId, playlistId, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				stations.getStation(stationId, next);
 				stations.getStation(stationId, next);
@@ -1181,7 +1159,7 @@ module.exports = {
 		});
 		});
 	}),
 	}),
 
 
-	favoriteStation: hooks.loginRequired((session, stationId, cb, userId) => {
+	favoriteStation: hooks.loginRequired((session, stationId, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				stations.getStation(stationId, next);
 				stations.getStation(stationId, next);
@@ -1189,32 +1167,15 @@ module.exports = {
 
 
 			(station, next) => {
 			(station, next) => {
 				if (!station) return next('Station not found.');
 				if (!station) return next('Station not found.');
-				async.waterfall([
-					(next) => {
-						if (station.privacy !== 'private') return next(true);
-						if (!session.userId) return next("You're not allowed to favorite this station.");
-						next();
-					},
-
-					(next) => {
-						db.models.user.findOne({ _id: userId }, next);
-					},
-
-					(user, next) => {
-						if (!user) return next("You're not allowed to favorite this station.");
-						if (user.role === 'admin') return next(true);
-						if (station.type === 'official') return next("You're not allowed to favorite this station.");
-						if (station.owner === session.userId) return next(true);
-						next("You're not allowed to favorite this station.");
-					}
-				], (err) => {
-					if (err === true) return next(null);
-					next(utils.getError(err));
+				stations.canUserViewStation(station, session.userId, (err, canView) => {
+					if (err) return next(err);
+					if (canView) return next();
+					return next('Insufficient permissions.');
 				});
 				});
 			},
 			},
 
 
 			(next) => {
 			(next) => {
-				db.models.user.updateOne({ _id: userId }, { $addToSet: { favoriteStations: stationId } }, next);
+				db.models.user.updateOne({ _id: session.userId }, { $addToSet: { favoriteStations: stationId } }, next);
 			},
 			},
 
 
 			(res, next) => {
 			(res, next) => {
@@ -1228,15 +1189,15 @@ module.exports = {
 				return cb({'status': 'failure', 'message': err});
 				return cb({'status': 'failure', 'message': err});
 			}
 			}
 			logger.success("FAVORITE_STATION", `Favorited station "${stationId}" successfully.`);
 			logger.success("FAVORITE_STATION", `Favorited station "${stationId}" successfully.`);
-			cache.pub('user.favoritedStation', { userId, stationId });
+			cache.pub('user.favoritedStation', { userId: session.userId, stationId });
 			return cb({'status': 'success', 'message': 'Succesfully favorited station.'});
 			return cb({'status': 'success', 'message': 'Succesfully favorited station.'});
 		});
 		});
 	}),
 	}),
 
 
-	unfavoriteStation: hooks.loginRequired((session, stationId, cb, userId) => {
+	unfavoriteStation: hooks.loginRequired((session, stationId, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				db.models.user.updateOne({ _id: userId }, { $pull: { favoriteStations: stationId } }, next);
+				db.models.user.updateOne({ _id: session.userId }, { $pull: { favoriteStations: stationId } }, next);
 			},
 			},
 
 
 			(res, next) => {
 			(res, next) => {
@@ -1250,7 +1211,7 @@ module.exports = {
 				return cb({'status': 'failure', 'message': err});
 				return cb({'status': 'failure', 'message': err});
 			}
 			}
 			logger.success("UNFAVORITE_STATION", `Unfavorited station "${stationId}" successfully.`);
 			logger.success("UNFAVORITE_STATION", `Unfavorited station "${stationId}" successfully.`);
-			cache.pub('user.unfavoritedStation', { userId, stationId });
+			cache.pub('user.unfavoritedStation', { userId: session.userId, stationId });
 			return cb({'status': 'success', 'message': 'Succesfully unfavorited station.'});
 			return cb({'status': 'success', 'message': 'Succesfully unfavorited station.'});
 		});
 		});
 	}),
 	}),

+ 49 - 59
backend/logic/actions/users.js

@@ -339,15 +339,15 @@ module.exports = {
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {String} userId - the id of the user we are trying to delete the sessions of
 	 * @param {String} userId - the id of the user we are trying to delete the sessions of
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} loggedInUser - the logged in userId automatically added by hooks
 	 */
 	 */
-	removeSessions:  hooks.loginRequired((session, userId, cb, loggedInUser) => {
+	removeSessions:  hooks.loginRequired((session, userId, cb) => {
 
 
 		async.waterfall([
 		async.waterfall([
 
 
 			(next) => {
 			(next) => {
-				db.models.user.findOne({ _id: loggedInUser }, (err, user) => {
-					if (user.role !== 'admin' && loggedInUser !== userId) return next('Only admins and the owner of the account can remove their sessions.');
+				db.models.user.findOne({ _id: session.userId }, (err, user) => {
+					if (err) return next(err);
+					if (user.role !== 'admin' && session.userId !== userId) return next('Only admins and the owner of the account can remove their sessions.');
 					else return next();
 					else return next();
 				});
 				});
 			},
 			},
@@ -522,13 +522,12 @@ module.exports = {
 	 * @param {String} updatingUserId - the updating user's id
 	 * @param {String} updatingUserId - the updating user's id
 	 * @param {String} newUsername - the new username
 	 * @param {String} newUsername - the new username
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	updateUsername: hooks.loginRequired((session, updatingUserId, newUsername, cb, userId) => {
+	updateUsername: hooks.loginRequired((session, updatingUserId, newUsername, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				if (updatingUserId === userId) return next(null, true);
-				db.models.user.findOne({_id: userId}, next);
+				if (updatingUserId === session.userId) return next(null, true);
+				db.models.user.findOne({_id: session.userId}, next);
 			},
 			},
 
 
 			(user, next) => {
 			(user, next) => {
@@ -578,15 +577,14 @@ module.exports = {
 	 * @param {String} updatingUserId - the updating user's id
 	 * @param {String} updatingUserId - the updating user's id
 	 * @param {String} newEmail - the new email
 	 * @param {String} newEmail - the new email
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	updateEmail: hooks.loginRequired(async (session, updatingUserId, newEmail, cb, userId) => {
+	updateEmail: hooks.loginRequired(async (session, updatingUserId, newEmail, cb) => {
 		newEmail = newEmail.toLowerCase();
 		newEmail = newEmail.toLowerCase();
 		let verificationToken = await utils.generateRandomString(64);
 		let verificationToken = await utils.generateRandomString(64);
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				if (updatingUserId === userId) return next(null, true);
-				db.models.user.findOne({_id: userId}, next);
+				if (updatingUserId === session.userId) return next(null, true);
+				db.models.user.findOne({_id: session.userId}, next);
 			},
 			},
 
 
 			(user, next) => {
 			(user, next) => {
@@ -642,9 +640,8 @@ module.exports = {
 	 * @param {String} updatingUserId - the updating user's id
 	 * @param {String} updatingUserId - the updating user's id
 	 * @param {String} newRole - the new role
 	 * @param {String} newRole - the new role
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	updateRole: hooks.adminRequired((session, updatingUserId, newRole, cb, userId) => {
+	updateRole: hooks.adminRequired((session, updatingUserId, newRole, cb) => {
 		newRole = newRole.toLowerCase();
 		newRole = newRole.toLowerCase();
 		async.waterfall([
 		async.waterfall([
 
 
@@ -664,10 +661,10 @@ module.exports = {
 		], async (err) => {
 		], async (err) => {
 			if (err && err !== true) {
 			if (err && err !== true) {
 				err = await 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}"`);
+				logger.error("UPDATE_ROLE", `User "${session.userId}" couldn't update role for user "${updatingUserId}" to role "${newRole}". "${err}"`);
 				cb({status: 'failure', message: err});
 				cb({status: 'failure', message: err});
 			} else {
 			} else {
-				logger.success("UPDATE_ROLE", `User "${userId}" updated the role of user "${updatingUserId}" to role "${newRole}".`);
+				logger.success("UPDATE_ROLE", `User "${session.userId}" updated the role of user "${updatingUserId}" to role "${newRole}".`);
 				cb({
 				cb({
 					status: 'success',
 					status: 'success',
 					message: 'Role successfully updated.'
 					message: 'Role successfully updated.'
@@ -682,12 +679,11 @@ module.exports = {
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {String} newPassword - the new password
 	 * @param {String} newPassword - the new password
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	updatePassword: hooks.loginRequired((session, newPassword, cb, userId) => {
+	updatePassword: hooks.loginRequired((session, newPassword, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				db.models.user.findOne({_id: userId}, next);
+				db.models.user.findOne({_id: session.userId}, next);
 			},
 			},
 
 
 			(user, next) => {
 			(user, next) => {
@@ -710,16 +706,16 @@ module.exports = {
 			},
 			},
 
 
 			(hashedPassword, next) => {
 			(hashedPassword, next) => {
-				db.models.user.updateOne({_id: userId}, {$set: {"services.password.password": hashedPassword}}, next);
+				db.models.user.updateOne({_id: session.userId}, {$set: {"services.password.password": hashedPassword}}, next);
 			}
 			}
 		], async (err) => {
 		], async (err) => {
 			if (err) {
 			if (err) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("UPDATE_PASSWORD", `Failed updating user password of user '${userId}'. '${err}'.`);
+				logger.error("UPDATE_PASSWORD", `Failed updating user password of user '${session.userId}'. '${err}'.`);
 				return cb({ status: 'failure', message: err });
 				return cb({ status: 'failure', message: err });
 			}
 			}
 
 
-			logger.success("UPDATE_PASSWORD", `User '${userId}' updated their password.`);
+			logger.success("UPDATE_PASSWORD", `User '${session.userId}' updated their password.`);
 			cb({
 			cb({
 				status: 'success',
 				status: 'success',
 				message: 'Password successfully updated.'
 				message: 'Password successfully updated.'
@@ -733,13 +729,12 @@ module.exports = {
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {String} email - the email of the user that requests a password reset
 	 * @param {String} email - the email of the user that requests a password reset
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	requestPassword: hooks.loginRequired(async (session, cb, userId) => {
+	requestPassword: hooks.loginRequired(async (session, cb) => {
 		let code = await utils.generateRandomString(8);
 		let code = await utils.generateRandomString(8);
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				db.models.user.findOne({_id: userId}, next);
+				db.models.user.findOne({_id: session.userId}, next);
 			},
 			},
 
 
 			(user, next) => {
 			(user, next) => {
@@ -760,10 +755,10 @@ module.exports = {
 		], async (err) => {
 		], async (err) => {
 			if (err && err !== true) {
 			if (err && err !== true) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("REQUEST_PASSWORD", `UserId '${userId}' failed to request password. '${err}'`);
+				logger.error("REQUEST_PASSWORD", `UserId '${session.userId}' failed to request password. '${err}'`);
 				cb({status: 'failure', message: err});
 				cb({status: 'failure', message: err});
 			} else {
 			} else {
-				logger.success("REQUEST_PASSWORD", `UserId '${userId}' successfully requested a password.`);
+				logger.success("REQUEST_PASSWORD", `UserId '${session.userId}' successfully requested a password.`);
 				cb({
 				cb({
 					status: 'success',
 					status: 'success',
 					message: 'Successfully requested password.'
 					message: 'Successfully requested password.'
@@ -778,13 +773,12 @@ module.exports = {
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {String} code - the password code
 	 * @param {String} code - the password code
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	verifyPasswordCode: hooks.loginRequired((session, code, cb, userId) => {
+	verifyPasswordCode: hooks.loginRequired((session, code, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				if (!code || typeof code !== 'string') return next('Invalid code1.');
 				if (!code || typeof code !== 'string') return next('Invalid code1.');
-				db.models.user.findOne({"services.password.set.code": code, _id: userId}, next);
+				db.models.user.findOne({"services.password.set.code": code, _id: session.userId}, next);
 			},
 			},
 
 
 			(user, next) => {
 			(user, next) => {
@@ -814,9 +808,8 @@ module.exports = {
 	 * @param {String} code - the password code
 	 * @param {String} code - the password code
 	 * @param {String} newPassword - the new password code
 	 * @param {String} newPassword - the new password code
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	changePasswordWithCode: hooks.loginRequired((session, code, newPassword, cb, userId) => {
+	changePasswordWithCode: hooks.loginRequired((session, code, newPassword, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
 				if (!code || typeof code !== 'string') return next('Invalid code1.');
 				if (!code || typeof code !== 'string') return next('Invalid code1.');
@@ -853,7 +846,7 @@ module.exports = {
 				cb({status: 'failure', message: err});
 				cb({status: 'failure', message: err});
 			} else {
 			} else {
 				logger.success("ADD_PASSWORD_WITH_CODE", `Code '${code}' successfully added password.`);
 				logger.success("ADD_PASSWORD_WITH_CODE", `Code '${code}' successfully added password.`);
-				cache.pub('user.linkPassword', userId);
+				cache.pub('user.linkPassword', session.userId);
 				cb({
 				cb({
 					status: 'success',
 					status: 'success',
 					message: 'Successfully added password.'
 					message: 'Successfully added password.'
@@ -867,27 +860,26 @@ module.exports = {
 	 *
 	 *
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	unlinkPassword: hooks.loginRequired((session, cb, userId) => {
+	unlinkPassword: hooks.loginRequired((session, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				db.models.user.findOne({_id: userId}, next);
+				db.models.user.findOne({_id: session.userId}, next);
 			},
 			},
 
 
 			(user, next) => {
 			(user, next) => {
 				if (!user) return next('Not logged in.');
 				if (!user) return next('Not logged in.');
 				if (!user.services.github || !user.services.github.id) return next('You can\'t remove password login without having GitHub login.');
 				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);
+				db.models.user.updateOne({_id: session.userId}, {$unset: {"services.password": ''}}, next);
 			}
 			}
 		], async (err) => {
 		], async (err) => {
 			if (err && err !== true) {
 			if (err && err !== true) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("UNLINK_PASSWORD", `Unlinking password failed for userId '${userId}'. '${err}'`);
+				logger.error("UNLINK_PASSWORD", `Unlinking password failed for userId '${session.userId}'. '${err}'`);
 				cb({status: 'failure', message: err});
 				cb({status: 'failure', message: err});
 			} else {
 			} else {
-				logger.success("UNLINK_PASSWORD", `Unlinking password successful for userId '${userId}'.`);
-				cache.pub('user.unlinkPassword', userId);
+				logger.success("UNLINK_PASSWORD", `Unlinking password successful for userId '${session.userId}'.`);
+				cache.pub('user.unlinkPassword', session.userId);
 				cb({
 				cb({
 					status: 'success',
 					status: 'success',
 					message: 'Successfully unlinked password.'
 					message: 'Successfully unlinked password.'
@@ -901,27 +893,26 @@ module.exports = {
 	 *
 	 *
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Object} session - the session object automatically added by socket.io
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	unlinkGitHub: hooks.loginRequired((session, cb, userId) => {
+	unlinkGitHub: hooks.loginRequired((session, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				db.models.user.findOne({_id: userId}, next);
+				db.models.user.findOne({_id: session.userId}, next);
 			},
 			},
 
 
 			(user, next) => {
 			(user, next) => {
 				if (!user) return next('Not logged in.');
 				if (!user) return next('Not logged in.');
 				if (!user.services.password || !user.services.password.password) return next('You can\'t remove GitHub login without having password login.');
 				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);
+				db.models.user.updateOne({_id: session.userId}, {$unset: {"services.github": ''}}, next);
 			}
 			}
 		], async (err) => {
 		], async (err) => {
 			if (err && err !== true) {
 			if (err && err !== true) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("UNLINK_GITHUB", `Unlinking GitHub failed for userId '${userId}'. '${err}'`);
+				logger.error("UNLINK_GITHUB", `Unlinking GitHub failed for userId '${session.userId}'. '${err}'`);
 				cb({status: 'failure', message: err});
 				cb({status: 'failure', message: err});
 			} else {
 			} else {
-				logger.success("UNLINK_GITHUB", `Unlinking GitHub successful for userId '${userId}'.`);
-				cache.pub('user.unlinkGitHub', userId);
+				logger.success("UNLINK_GITHUB", `Unlinking GitHub successful for userId '${session.userId}'.`);
+				cache.pub('user.unlinkGitHub', session.userId);
 				cb({
 				cb({
 					status: 'success',
 					status: 'success',
 					message: 'Successfully unlinked GitHub.'
 					message: 'Successfully unlinked GitHub.'
@@ -1071,13 +1062,12 @@ module.exports = {
 	 * @param {String} reason - the reason for the ban
 	 * @param {String} reason - the reason for the ban
 	 * @param {String} expiresAt - the time the ban expires
 	 * @param {String} expiresAt - the time the ban expires
 	 * @param {Function} cb - gets called with the result
 	 * @param {Function} cb - gets called with the result
-	 * @param {String} userId - the userId automatically added by hooks
 	 */
 	 */
-	banUserById: hooks.adminRequired((session, value, reason, expiresAt, cb, userId) => {
+	banUserById: hooks.adminRequired((session, userId, reason, expiresAt, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				if (value === '') return next('You must provide an IP address to ban.');
-				else if (reason === '') return next('You must provide a reason for the ban.');
+				if (!userId) return next('You must provide a userId to ban.');
+				else if (!reason) return next('You must provide a reason for the ban.');
 				else return next();
 				else return next();
 			},
 			},
 
 
@@ -1120,20 +1110,20 @@ module.exports = {
 			},
 			},
 
 
 			(next) => {
 			(next) => {
-				punishments.addPunishment('banUserId', value, reason, expiresAt, userId, next)
+				punishments.addPunishment('banUserId', userId, reason, expiresAt, userId, next)
 			},
 			},
 
 
 			(punishment, next) => {
 			(punishment, next) => {
-				cache.pub('user.ban', {userId: value, punishment});
+				cache.pub('user.ban', { userId, punishment });
 				next();
 				next();
 			},
 			},
 		], async (err) => {
 		], async (err) => {
 			if (err && err !== true) {
 			if (err && err !== true) {
 				err = await 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}'`);
+				logger.error("BAN_USER_BY_ID", `User ${session.userId} failed to ban user ${userId} with the reason ${reason}. '${err}'`);
 				cb({status: 'failure', message: err});
 				cb({status: 'failure', message: err});
 			} else {
 			} else {
-				logger.success("BAN_USER_BY_ID", `User ${userId} has successfully banned user ${value} with the reason ${reason}.`);
+				logger.success("BAN_USER_BY_ID", `User ${session.userId} has successfully banned user ${userId} with the reason ${reason}.`);
 				cb({
 				cb({
 					status: 'success',
 					status: 'success',
 					message: 'Successfully banned user.'
 					message: 'Successfully banned user.'
@@ -1142,10 +1132,10 @@ module.exports = {
 		});
 		});
 	}),
 	}),
 
 
-	getFavoriteStations: hooks.loginRequired((session, cb, userId) => {
+	getFavoriteStations: hooks.loginRequired((session, cb) => {
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				db.models.user.findOne({ _id: userId }, next);
+				db.models.user.findOne({ _id: session.userId }, next);
 			},
 			},
 
 
 			(user, next) => {
 			(user, next) => {
@@ -1155,10 +1145,10 @@ module.exports = {
 		], async (err, user) => {
 		], async (err, user) => {
 			if (err && err !== true) {
 			if (err && err !== true) {
 				err = await utils.getError(err);
 				err = await utils.getError(err);
-				logger.error("GET_FAVORITE_STATIONS", `User ${userId} failed to get favorite stations. '${err}'`);
+				logger.error("GET_FAVORITE_STATIONS", `User ${session.userId} failed to get favorite stations. '${err}'`);
 				cb({status: 'failure', message: err});
 				cb({status: 'failure', message: err});
 			} else {
 			} else {
-				logger.success("GET_FAVORITE_STATIONS", `User ${userId} got favorite stations.`);
+				logger.success("GET_FAVORITE_STATIONS", `User ${session.userId} got favorite stations.`);
 				cb({
 				cb({
 					status: 'success',
 					status: 'success',
 					favoriteStations: user.favoriteStations
 					favoriteStations: user.favoriteStations

+ 1 - 1
backend/logic/cache/index.js

@@ -138,7 +138,7 @@ module.exports = class extends coreClass {
 	async hdel(table, key, cb) {
 	async hdel(table, key, cb) {
 		try { await this._validateHook(); } catch { return; }
 		try { await this._validateHook(); } catch { return; }
 
 
-		if (!key || !table) return cb(null, null);
+		if (!key || !table || typeof key !== "string") return cb(null, null);
 		if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();
 		if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();
 
 
 		this.client.hdel(table, key, (err) => {
 		this.client.hdel(table, key, (err) => {

+ 39 - 30
backend/logic/db/index.js

@@ -9,8 +9,8 @@ const regex = {
 	azAZ09_: /^[A-Za-z0-9_]+$/,
 	azAZ09_: /^[A-Za-z0-9_]+$/,
 	az09_: /^[a-z0-9_]+$/,
 	az09_: /^[a-z0-9_]+$/,
 	emailSimple: /^[\x00-\x7F]+@[a-z0-9]+\.[a-z0-9]+(\.[a-z0-9]+)?$/,
 	emailSimple: /^[\x00-\x7F]+@[a-z0-9]+\.[a-z0-9]+(\.[a-z0-9]+)?$/,
-	password: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!%*?&])[A-Za-z\d$@$!%*?&]/,
-	ascii: /^[\x00-\x7F]+$/
+	ascii: /^[\x00-\x7F]+$/,
+	custom: regex => new RegExp(`^[${regex}]+$`)
 };
 };
 
 
 const isLength = (string, min, max) => {
 const isLength = (string, min, max) => {
@@ -80,22 +80,24 @@ module.exports = class extends coreClass {
 						this._lockdown();
 						this._lockdown();
 					});
 					});
 		
 		
-					// this.schemas.user.path('username').validate((username) => {
-					// 	return (isLength(username, 2, 32) && regex.azAZ09_.test(username));
-					// }, 'Invalid username.');
+					// User
+					this.schemas.user.path('username').validate((username) => {
+						return (isLength(username, 2, 32) && regex.custom("a-zA-Z0-9_-").test(username));
+					}, 'Invalid username.');
 		
 		
 					this.schemas.user.path('email.address').validate((email) => {
 					this.schemas.user.path('email.address').validate((email) => {
 						if (!isLength(email, 3, 254)) return false;
 						if (!isLength(email, 3, 254)) return false;
 						if (email.indexOf('@') !== email.lastIndexOf('@')) return false;
 						if (email.indexOf('@') !== email.lastIndexOf('@')) return false;
-						return regex.emailSimple.test(email);
+						return regex.emailSimple.test(email) && regex.ascii.test(email);
 					}, 'Invalid email.');
 					}, 'Invalid email.');
-		
+
+					// Station
 					this.schemas.station.path('name').validate((id) => {
 					this.schemas.station.path('name').validate((id) => {
 						return (isLength(id, 2, 16) && regex.az09_.test(id));
 						return (isLength(id, 2, 16) && regex.az09_.test(id));
 					}, 'Invalid station name.');
 					}, 'Invalid station name.');
 		
 		
 					this.schemas.station.path('displayName').validate((displayName) => {
 					this.schemas.station.path('displayName').validate((displayName) => {
-						return (isLength(displayName, 2, 32) && regex.azAZ09_.test(displayName));
+						return (isLength(displayName, 2, 32) && regex.ascii.test(displayName));
 					}, 'Invalid display name.');
 					}, 'Invalid display name.');
 		
 		
 					this.schemas.station.path('description').validate((description) => {
 					this.schemas.station.path('description').validate((description) => {
@@ -106,12 +108,14 @@ module.exports = class extends coreClass {
 						}).length === 0;
 						}).length === 0;
 					}, 'Invalid display name.');
 					}, 'Invalid display name.');
 		
 		
-		
 					this.schemas.station.path('owner').validate({
 					this.schemas.station.path('owner').validate({
-						isAsync: true,
-						validator: (owner, callback) => {
-							this.models.station.countDocuments({ owner: owner }, (err, c) => {
-								callback(!(err || c >= 3))
+						validator: (owner) => {
+							return new Promise((resolve, reject) => {
+								this.models.station.countDocuments({ owner: owner }, (err, c) => {
+									if (err) reject(new Error("A mongo error happened."));
+									else if (c >= 3) reject(new Error("User already has 3 stations."));
+									else resolve();
+								});
 							});
 							});
 						},
 						},
 						message: 'User already has 3 stations.'
 						message: 'User already has 3 stations.'
@@ -153,7 +157,9 @@ module.exports = class extends coreClass {
 						return callback(false);
 						return callback(false);
 					}, 'The max amount of songs per user is 3, and only 2 in a row is allowed.');
 					}, 'The max amount of songs per user is 3, and only 2 in a row is allowed.');
 					*/
 					*/
-		
+
+
+					// Song
 					let songTitle = (title) => {
 					let songTitle = (title) => {
 						return isLength(title, 1, 100);
 						return isLength(title, 1, 100);
 					};
 					};
@@ -169,29 +175,32 @@ module.exports = class extends coreClass {
 		
 		
 					let songArtists = (artists) => {
 					let songArtists = (artists) => {
 						return artists.filter((artist) => {
 						return artists.filter((artist) => {
-								return (isLength(artist, 1, 32) && regex.ascii.test(artist) && artist !== "NONE");
+								return (isLength(artist, 1, 64) && artist !== "NONE");
 							}).length === artists.length;
 							}).length === artists.length;
 					};
 					};
 					this.schemas.song.path('artists').validate(songArtists, 'Invalid artists.');
 					this.schemas.song.path('artists').validate(songArtists, 'Invalid artists.');
 					this.schemas.queueSong.path('artists').validate(songArtists, 'Invalid artists.');
 					this.schemas.queueSong.path('artists').validate(songArtists, 'Invalid artists.');
 		
 		
-					/*let songGenres = (genres) => {
+					let songGenres = (genres) => {
+						if (genres.length < 1 || genres.length > 16) return false;
 						return genres.filter((genre) => {
 						return genres.filter((genre) => {
-								return (isLength(genre, 1, 16) && regex.azAZ09_.test(genre));
+								return (isLength(genre, 1, 32) && regex.ascii.test(genre));
 							}).length === genres.length;
 							}).length === genres.length;
 					};
 					};
 					this.schemas.song.path('genres').validate(songGenres, 'Invalid genres.');
 					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.queueSong.path('genres').validate(songGenres, 'Invalid genres.');
 		
 		
+					let songThumbnail = (thumbnail) => {
+						if (!isLength(thumbnail, 1, 256)) return false;
+						if (config.get("cookie.secure") === true) return thumbnail.startsWith("https://");
+						else return thumbnail.startsWith("http://") || thumbnail.startsWith("https://");
+					};
+					this.schemas.song.path('thumbnail').validate(songThumbnail, 'Invalid thumbnail.');
+					this.schemas.queueSong.path('thumbnail').validate(songThumbnail, 'Invalid thumbnail.');
+
+					// Playlist
 					this.schemas.playlist.path('displayName').validate((displayName) => {
 					this.schemas.playlist.path('displayName').validate((displayName) => {
-						return (isLength(displayName, 1, 16) && regex.ascii.test(displayName));
+						return (isLength(displayName, 1, 32) && regex.ascii.test(displayName));
 					}, 'Invalid display name.');
 					}, 'Invalid display name.');
 		
 		
 					this.schemas.playlist.path('createdBy').validate((createdBy) => {
 					this.schemas.playlist.path('createdBy').validate((createdBy) => {
@@ -201,14 +210,15 @@ module.exports = class extends coreClass {
 					}, 'Max 10 playlists per user.');
 					}, 'Max 10 playlists per user.');
 		
 		
 					this.schemas.playlist.path('songs').validate((songs) => {
 					this.schemas.playlist.path('songs').validate((songs) => {
-						return songs.length <= 2000;
-					}, 'Max 2000 songs per playlist.');
+						return songs.length <= 5000;
+					}, 'Max 5000 songs per playlist.');
 		
 		
 					this.schemas.playlist.path('songs').validate((songs) => {
 					this.schemas.playlist.path('songs').validate((songs) => {
 						if (songs.length === 0) return true;
 						if (songs.length === 0) return true;
 						return songs[0].duration <= 10800;
 						return songs[0].duration <= 10800;
 					}, 'Max 3 hours per song.');
 					}, 'Max 3 hours per song.');
 		
 		
+					// Report
 					this.schemas.report.path('description').validate((description) => {
 					this.schemas.report.path('description').validate((description) => {
 						return (!description || (isLength(description, 0, 400) && regex.ascii.test(description)));
 						return (!description || (isLength(description, 0, 400) && regex.ascii.test(description)));
 					}, 'Invalid description.');
 					}, 'Invalid description.');
@@ -223,7 +233,6 @@ module.exports = class extends coreClass {
 	}
 	}
 
 
 	passwordValid(password) {
 	passwordValid(password) {
-		if (!isLength(password, 6, 200)) return false;
-		return regex.password.test(password);
+		return isLength(password, 6, 200);
 	}
 	}
 }
 }

+ 1 - 1
backend/logic/discord.js

@@ -50,7 +50,7 @@ module.exports = class extends coreClass {
 	async sendAdminAlertMessage(message, color, type, critical, extraFields) {
 	async sendAdminAlertMessage(message, color, type, critical, extraFields) {
 		try { await this._validateHook(); } catch { return; }
 		try { await this._validateHook(); } catch { return; }
 
 
-		const channel = this.client.channels.find("id", this.adminAlertChannelId);
+		const channel = this.client.channels.find(channel => channel.id === this.adminAlertChannelId);
 		if (channel !== null) {
 		if (channel !== null) {
 			let richEmbed = new Discord.RichEmbed();
 			let richEmbed = new Discord.RichEmbed();
 			richEmbed.setAuthor(
 			richEmbed.setAuthor(

+ 8 - 3
backend/logic/io.js

@@ -31,9 +31,9 @@ module.exports = class extends coreClass {
 			const SIDname = config.get("cookie.SIDname");
 			const SIDname = config.get("cookie.SIDname");
 
 
 			// 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)
 			// 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);
+			this._io = socketio(app.server);
 
 
-			this.io.use(async (socket, next) => {
+			this._io.use(async (socket, next) => {
 				try { await this._validateHook(); } catch { return; }
 				try { await this._validateHook(); } catch { return; }
 
 
 				let SID;
 				let SID;
@@ -95,7 +95,7 @@ module.exports = class extends coreClass {
 				});
 				});
 			});
 			});
 
 
-			this.io.on('connection', async socket => {
+			this._io.on('connection', async socket => {
 				try { await this._validateHook(); } catch { return; }
 				try { await this._validateHook(); } catch { return; }
 
 
 				let sessionInfo = '';
 				let sessionInfo = '';
@@ -186,4 +186,9 @@ module.exports = class extends coreClass {
 			resolve();
 			resolve();
 		});
 		});
 	}
 	}
+
+	async io () {
+		try { await this._validateHook(); } catch { return; }
+		return this._io;
+	}
 }
 }

+ 2 - 1
backend/logic/notifications.js

@@ -89,8 +89,9 @@ module.exports = class extends coreClass {
 			});
 			});
 
 
 			this.sub.on('pmessage', (pattern, channel, expiredKey) => {
 			this.sub.on('pmessage', (pattern, channel, expiredKey) => {
-				this.logger.stationIssue(`PMESSAGE - Pattern: ${pattern}; Channel: ${channel}; ExpiredKey: ${expiredKey}`);
+				this.logger.stationIssue(`PMESSAGE1 - Pattern: ${pattern}; Channel: ${channel}; ExpiredKey: ${expiredKey}`);
 				subscriptions.forEach((sub) => {
 				subscriptions.forEach((sub) => {
+					this.logger.stationIssue(`PMESSAGE2 - Sub name: ${sub.name}; Calls cb: ${!(sub.name !== expiredKey)}`);
 					if (sub.name !== expiredKey) return;
 					if (sub.name !== expiredKey) return;
 					sub.cb();
 					sub.cb();
 				});
 				});

+ 57 - 27
backend/logic/stations.js

@@ -92,18 +92,20 @@ module.exports = class extends coreClass {
 				},
 				},
 	
 	
 				(stations, next) => {
 				(stations, next) => {
-					this.setStage(4);
-					async.each(stations, (station, next) => {
+					this.setStage(5);
+					async.each(stations, (station, next2) => {
 						async.waterfall([
 						async.waterfall([
 							(next) => {
 							(next) => {
 								this.cache.hset('stations', station._id, this.cache.schemas.station(station), next);
 								this.cache.hset('stations', station._id, this.cache.schemas.station(station), next);
 							},
 							},
 	
 	
 							(station, next) => {
 							(station, next) => {
-								this.initializeStation(station._id, next);
+								this.initializeStation(station._id, () => {
+									next()
+								}, true);
 							}
 							}
 						], (err) => {
 						], (err) => {
-							next(err);
+							next2(err);
 						});
 						});
 					}, next);
 					}, next);
 				}
 				}
@@ -118,14 +120,14 @@ module.exports = class extends coreClass {
 		});
 		});
 	}
 	}
 
 
-	async initializeStation(stationId, cb) {
-		try { await this._validateHook(); } catch { return; }
+	async initializeStation(stationId, cb, bypassValidate = false) {
+		if (!bypassValidate) try { await this._validateHook(); } catch { return; }
 
 
 		if (typeof cb !== 'function') cb = ()=>{};
 		if (typeof cb !== 'function') cb = ()=>{};
 
 
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
-				this.getStation(stationId, next);
+				this.getStation(stationId, next, true);
 			},
 			},
 			(station, next) => {
 			(station, next) => {
 				if (!station) return next('Station not found.');
 				if (!station) return next('Station not found.');
@@ -139,14 +141,14 @@ module.exports = class extends coreClass {
 					return this.skipStation(station._id)((err, station) => {
 					return this.skipStation(station._id)((err, station) => {
 						if (err) return next(err);
 						if (err) return next(err);
 						return next(true, station);
 						return next(true, station);
-					});
+					}, true);
 				}
 				}
 				let timeLeft = ((station.currentSong.duration * 1000) - (Date.now() - station.startedAt - station.timePaused));
 				let timeLeft = ((station.currentSong.duration * 1000) - (Date.now() - station.startedAt - station.timePaused));
 				if (isNaN(timeLeft)) timeLeft = -1;
 				if (isNaN(timeLeft)) timeLeft = -1;
 				if (station.currentSong.duration * 1000 < timeLeft || timeLeft < 0) {
 				if (station.currentSong.duration * 1000 < timeLeft || timeLeft < 0) {
 					this.skipStation(station._id)((err, station) => {
 					this.skipStation(station._id)((err, station) => {
 						next(err, station);
 						next(err, station);
-					});
+					}, true);
 				} else {
 				} else {
 					this.notifications.schedule(`stations.nextSong?id=${station._id}`, timeLeft, null, station);
 					this.notifications.schedule(`stations.nextSong?id=${station._id}`, timeLeft, null, station);
 					next(null, station);
 					next(null, station);
@@ -158,12 +160,13 @@ module.exports = class extends coreClass {
 		});
 		});
 	}
 	}
 
 
-	async calculateSongForStation(station, cb) {
-		try { await this._validateHook(); } catch { return; }
+	async calculateSongForStation(station, cb, bypassValidate = false) {
+		if (!bypassValidate) try { await this._validateHook(); } catch { return; }
 
 
 		let songList = [];
 		let songList = [];
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
+				if (station.genres.length === 0) return next();
 				let genresDone = [];
 				let genresDone = [];
 				station.genres.forEach((genre) => {
 				station.genres.forEach((genre) => {
 					this.db.models.song.find({genres: genre}, (err, songs) => {
 					this.db.models.song.find({genres: genre}, (err, songs) => {
@@ -203,14 +206,14 @@ module.exports = class extends coreClass {
 			(playlist, next) => {
 			(playlist, next) => {
 				this.calculateOfficialPlaylistList(station._id, playlist, () => {
 				this.calculateOfficialPlaylistList(station._id, playlist, () => {
 					next(null, playlist);
 					next(null, playlist);
-				});
+				}, true);
 			},
 			},
 
 
 			(playlist, next) => {
 			(playlist, next) => {
 				this.db.models.station.updateOne({_id: station._id}, {$set: {playlist: playlist}}, {runValidators: true}, (err) => {
 				this.db.models.station.updateOne({_id: station._id}, {$set: {playlist: playlist}}, {runValidators: true}, (err) => {
 					this.updateStation(station._id, () => {
 					this.updateStation(station._id, () => {
 						next(err, playlist);
 						next(err, playlist);
-					});
+					}, true);
 				});
 				});
 			}
 			}
 
 
@@ -220,8 +223,8 @@ module.exports = class extends coreClass {
 	}
 	}
 
 
 	// Attempts to get the station from Redis. If it's not in Redis, get it from Mongo and add it to Redis.
 	// Attempts to get the station from Redis. If it's not in Redis, get it from Mongo and add it to Redis.
-	async getStation(stationId, cb) {
-		try { await this._validateHook(); } catch { return; }
+	async getStation(stationId, cb, bypassValidate = false) {
+		if (!bypassValidate) try { await this._validateHook(); } catch { return; }
 
 
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
@@ -277,8 +280,8 @@ module.exports = class extends coreClass {
 		});
 		});
 	}
 	}
 
 
-	async updateStation(stationId, cb) {
-		try { await this._validateHook(); } catch { return; }
+	async updateStation(stationId, cb, bypassValidate = false) {
+		if (!bypassValidate) try { await this._validateHook(); } catch { return; }
 
 
 		async.waterfall([
 		async.waterfall([
 
 
@@ -300,8 +303,8 @@ module.exports = class extends coreClass {
 		});
 		});
 	}
 	}
 
 
-	async calculateOfficialPlaylistList(stationId, songList, cb) {
-		try { await this._validateHook(); } catch { return; }
+	async calculateOfficialPlaylistList(stationId, songList, cb, bypassValidate = false) {
+		if (!bypassValidate) try { await this._validateHook(); } catch { return; }
 
 
 		let lessInfoPlaylist = [];
 		let lessInfoPlaylist = [];
 		async.each(songList, (song, next) => {
 		async.each(songList, (song, next) => {
@@ -327,14 +330,15 @@ module.exports = class extends coreClass {
 
 
 	skipStation(stationId) {
 	skipStation(stationId) {
 		this.logger.info("STATION_SKIP", `Skipping station ${stationId}.`, false);
 		this.logger.info("STATION_SKIP", `Skipping station ${stationId}.`, false);
-		return async (cb) => {
-			try { await this._validateHook(); } catch { return; }
+		return async (cb, bypassValidate = false) => {
+			if (!bypassValidate) try { await this._validateHook(); } catch { return; }
+			this.logger.stationIssue(`SKIP_STATION_CB - Station ID: ${stationId}.`);
 
 
 			if (typeof cb !== 'function') cb = ()=>{};
 			if (typeof cb !== 'function') cb = ()=>{};
 
 
 			async.waterfall([
 			async.waterfall([
 				(next) => {
 				(next) => {
-					this.getStation(stationId, next);
+					this.getStation(stationId, next, true);
 				},
 				},
 				(station, next) => {
 				(station, next) => {
 					if (!station) return next('Station not found.');
 					if (!station) return next('Station not found.');
@@ -384,7 +388,7 @@ module.exports = class extends coreClass {
 									return next(null, song, 0, station);
 									return next(null, song, 0, station);
 								});
 								});
 							}
 							}
-						});
+						}, true);
 					}
 					}
 					if (station.type === 'official' && station.playlist.length > 0) {
 					if (station.type === 'official' && station.playlist.length > 0) {
 						async.doUntil((next) => {
 						async.doUntil((next) => {
@@ -393,7 +397,7 @@ module.exports = class extends coreClass {
 									if (!err) return next(null, song, station.currentSongIndex + 1);
 									if (!err) return next(null, song, station.currentSongIndex + 1);
 									else {
 									else {
 										station.currentSongIndex++;
 										station.currentSongIndex++;
-										next(null, null);
+										next(null, null, null);
 									}
 									}
 								});
 								});
 							} else {
 							} else {
@@ -404,7 +408,7 @@ module.exports = class extends coreClass {
 										station.playlist = newPlaylist;
 										station.playlist = newPlaylist;
 										next(null, song, 0);
 										next(null, song, 0);
 									});
 									});
-								});
+								}, true);
 							}
 							}
 						}, (song, currentSongIndex, next) => {
 						}, (song, currentSongIndex, next) => {
 							if (!!song) return next(null, true, currentSongIndex);
 							if (!!song) return next(null, true, currentSongIndex);
@@ -451,7 +455,7 @@ module.exports = class extends coreClass {
 							if (station.type === 'community' && station.partyMode === true)
 							if (station.type === 'community' && station.partyMode === true)
 								this.cache.pub('station.queueUpdate', stationId);
 								this.cache.pub('station.queueUpdate', stationId);
 							next(null, station);
 							next(null, station);
-						});
+						}, true);
 					});
 					});
 				},
 				},
 			], async (err, station) => {
 			], async (err, station) => {
@@ -498,10 +502,36 @@ module.exports = class extends coreClass {
 					cb(null, station);
 					cb(null, station);
 				} else {
 				} else {
 					err = await this.utils.getError(err);
 					err = await this.utils.getError(err);
-					logger.error('SKIP_STATION', `Skipping station "${stationId}" failed. "${err}"`);
+					this.logger.error('SKIP_STATION', `Skipping station "${stationId}" failed. "${err}"`);
 					cb(err);
 					cb(err);
 				}
 				}
 			});
 			});
 		}
 		}
 	}
 	}
+
+	async canUserViewStation(station, userId, cb) {
+		try { await this._validateHook(); } catch { return; }
+		async.waterfall([
+			(next) => {
+				if (station.privacy !== 'private') return next(true);
+				if (!userId) return next("Not allowed");
+				next();
+			},
+			
+			(next) => {
+				this.db.models.user.findOne({_id: userId}, next);
+			},
+			
+			(user, next) => {
+				if (!user) return next("Not allowed");
+				if (user.role === 'admin') return next(true);
+				if (station.type === 'official') return next("Not allowed");
+				if (station.owner === userId) return next(true);
+				next("Not allowed");
+			}
+		], async (errOrResult) => {
+			if (errOrResult === true || errOrResult === "Not allowed") return cb(null, (errOrResult === true) ? true : false);
+			cb(await this.utils.getError(errOrResult));
+		});
+	}
 }
 }

+ 33 - 11
backend/logic/tasks.js

@@ -3,6 +3,7 @@
 const coreClass = require("../core");
 const coreClass = require("../core");
 
 
 const async = require("async");
 const async = require("async");
+const fs = require("fs");
 
 
 let tasks = {};
 let tasks = {};
 
 
@@ -19,6 +20,7 @@ module.exports = class extends coreClass {
 			//this.createTask("testTask", testTask, 5000, true);
 			//this.createTask("testTask", testTask, 5000, true);
 			this.createTask("stationSkipTask", this.checkStationSkipTask, 1000 * 60 * 30);
 			this.createTask("stationSkipTask", this.checkStationSkipTask, 1000 * 60 * 30);
 			this.createTask("sessionClearTask", this.sessionClearingTask, 1000 * 60 * 60 * 6);
 			this.createTask("sessionClearTask", this.sessionClearingTask, 1000 * 60 * 60 * 6);
+			this.createTask("logFileSizeCheckTask", this.logFileSizeCheckTask, 1000 * 60 * 60);
 
 
 			resolve();
 			resolve();
 		});
 		});
@@ -53,13 +55,15 @@ module.exports = class extends coreClass {
 		try { await this._validateHook(); } catch { return; }
 		try { await this._validateHook(); } catch { return; }
 
 
 		if (task.timer) task.timer.pause();
 		if (task.timer) task.timer.pause();
-
-		task.fn(() => {
-			task.lastRan = Date.now();
-			task.timer = new utils.Timer(() => {
-				this.handleTask(task);
-			}, task.timeout, false);
-		});
+		
+		task.fn.apply(this, [
+			() => {
+				task.lastRan = Date.now();
+				task.timer = new this.utils.Timer(() => {
+					this.handleTask(task);
+				}, task.timeout, false);
+			}
+		]);
 	}
 	}
 
 
 	/*testTask(callback) {
 	/*testTask(callback) {
@@ -72,8 +76,6 @@ module.exports = class extends coreClass {
 	}*/
 	}*/
 
 
 	async checkStationSkipTask(callback) {
 	async checkStationSkipTask(callback) {
-		try { await this._validateHook(); } catch { return; }
-
 		this.logger.info("TASK_STATIONS_SKIP_CHECK", `Checking for stations to be skipped.`, false);
 		this.logger.info("TASK_STATIONS_SKIP_CHECK", `Checking for stations to be skipped.`, false);
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
@@ -99,8 +101,6 @@ module.exports = class extends coreClass {
 	}
 	}
 
 
 	async sessionClearingTask(callback) {
 	async sessionClearingTask(callback) {
-		try { await this._validateHook(); } catch { return; }
-	
 		this.logger.info("TASK_SESSION_CLEAR", `Checking for sessions to be cleared.`, false);
 		this.logger.info("TASK_SESSION_CLEAR", `Checking for sessions to be cleared.`, false);
 		async.waterfall([
 		async.waterfall([
 			(next) => {
 			(next) => {
@@ -148,4 +148,26 @@ module.exports = class extends coreClass {
 			callback();
 			callback();
 		});
 		});
 	}
 	}
+
+	async logFileSizeCheckTask(callback) {
+		this.logger.info("TASK_LOG_FILE_SIZE_CHECK", `Checking the size for the log files.`);
+		async.each(
+			["all.log", "debugStation.log", "error.log", "info.log", "success.log"],
+			(fileName, next) => {
+				const stats = fs.statSync(`${__dirname}/../../log/${fileName}`);
+				const mb = stats.size / 1000000;
+				if (mb > 25) return next(true);
+				else next();
+			},
+			(err) => {
+				if (err === true) {
+					this.logger.error("LOGGER_FILE_SIZE_WARNING", "************************************WARNING*************************************");
+					this.logger.error("LOGGER_FILE_SIZE_WARNING", "***************ONE OR MORE LOG FILES APPEAR TO BE MORE THAN 25MB****************");
+					this.logger.error("LOGGER_FILE_SIZE_WARNING", "****MAKE SURE TO REGULARLY CLEAR UP THE LOG FILES, MANUALLY OR AUTOMATICALLY****");
+					this.logger.error("LOGGER_FILE_SIZE_WARNING", "********************************************************************************");
+				}
+				callback();
+			}
+		);
+	}
 }
 }

+ 14 - 34
backend/logic/utils.js

@@ -172,7 +172,8 @@ module.exports = class extends coreClass {
 	async socketFromSession(socketId) {
 	async socketFromSession(socketId) {
 		try { await this._validateHook(); } catch { return; }
 		try { await this._validateHook(); } catch { return; }
 
 
-		let ns = this.io.io.of("/");
+		let io = await this.io.io();
+		let ns = io.of("/");
 		if (ns) {
 		if (ns) {
 			return ns.connected[socketId];
 			return ns.connected[socketId];
 		}
 		}
@@ -181,7 +182,8 @@ module.exports = class extends coreClass {
 	async socketsFromSessionId(sessionId, cb) {
 	async socketsFromSessionId(sessionId, cb) {
 		try { await this._validateHook(); } catch { return; }
 		try { await this._validateHook(); } catch { return; }
 
 
-		let ns = this.io.io.of("/");
+		let io = await this.io.io();
+		let ns = io.of("/");
 		let sockets = [];
 		let sockets = [];
 		if (ns) {
 		if (ns) {
 			async.each(Object.keys(ns.connected), (id, next) => {
 			async.each(Object.keys(ns.connected), (id, next) => {
@@ -197,7 +199,8 @@ module.exports = class extends coreClass {
 	async socketsFromUser(userId, cb) {
 	async socketsFromUser(userId, cb) {
 		try { await this._validateHook(); } catch { return; }
 		try { await this._validateHook(); } catch { return; }
 
 
-		let ns = this.io.io.of("/");
+		let io = await this.io.io();
+		let ns = io.of("/");
 		let sockets = [];
 		let sockets = [];
 		if (ns) {
 		if (ns) {
 			async.each(Object.keys(ns.connected), (id, next) => {
 			async.each(Object.keys(ns.connected), (id, next) => {
@@ -215,7 +218,8 @@ module.exports = class extends coreClass {
 	async socketsFromIP(ip, cb) {
 	async socketsFromIP(ip, cb) {
 		try { await this._validateHook(); } catch { return; }
 		try { await this._validateHook(); } catch { return; }
 
 
-		let ns = this.io.io.of("/");
+		let io = await this.io.io();
+		let ns = io.of("/");
 		let sockets = [];
 		let sockets = [];
 		if (ns) {
 		if (ns) {
 			async.each(Object.keys(ns.connected), (id, next) => {
 			async.each(Object.keys(ns.connected), (id, next) => {
@@ -233,7 +237,8 @@ module.exports = class extends coreClass {
 	async socketsFromUserWithoutCache(userId, cb) {
 	async socketsFromUserWithoutCache(userId, cb) {
 		try { await this._validateHook(); } catch { return; }
 		try { await this._validateHook(); } catch { return; }
 
 
-		let ns = this.io.io.of("/");
+		let io = await this.io.io();
+		let ns = io.of("/");
 		let sockets = [];
 		let sockets = [];
 		if (ns) {
 		if (ns) {
 			async.each(Object.keys(ns.connected), (id, next) => {
 			async.each(Object.keys(ns.connected), (id, next) => {
@@ -306,7 +311,8 @@ module.exports = class extends coreClass {
 	async emitToRoom(room, ...args) {
 	async emitToRoom(room, ...args) {
 		try { await this._validateHook(); } catch { return; }
 		try { await this._validateHook(); } catch { return; }
 
 
-		let sockets = this.io.io.sockets.sockets;
+		let io = await this.io.io();
+		let sockets = io.sockets.sockets;
 		for (let id in sockets) {
 		for (let id in sockets) {
 			let socket = sockets[id];
 			let socket = sockets[id];
 			if (socket.rooms[room]) {
 			if (socket.rooms[room]) {
@@ -318,7 +324,8 @@ module.exports = class extends coreClass {
 	async getRoomSockets(room) {
 	async getRoomSockets(room) {
 		try { await this._validateHook(); } catch { return; }
 		try { await this._validateHook(); } catch { return; }
 
 
-		let sockets = this.io.io.sockets.sockets;
+		let io = await this.io.io();
+		let sockets = io.sockets.sockets;
 		let roomSockets = [];
 		let roomSockets = [];
 		for (let id in sockets) {
 		for (let id in sockets) {
 			let socket = sockets[id];
 			let socket = sockets[id];
@@ -555,31 +562,4 @@ module.exports = class extends coreClass {
 		}
 		}
 		return error;
 		return error;
 	}
 	}
-
-	async canUserBeInStation(station, userId, cb) {
-		try { await this._validateHook(); } catch { return; }
-
-		async.waterfall([
-			(next) => {
-				if (station.privacy !== 'private') return next(true);
-				if (!userId) return next(false);
-				next();
-			},
-
-			(next) => {
-				this.db.models.user.findOne({_id: userId}, next);
-			},
-
-			(user, next) => {
-				if (!user) return next(false);
-				if (user.role === 'admin') return next(true);
-				if (station.type === 'official') return next(false);
-				if (station.owner === userId) return next(true);
-				next(false);
-			}
-		], (err) => {
-			if (err === true) return cb(true);
-			return cb(false);
-		});
-	}
 }
 }

+ 2 - 1
frontend/.eslintrc

@@ -29,6 +29,7 @@
 		"no-underscore-dangle": 0,
 		"no-underscore-dangle": 0,
 		"radix": 0,
 		"radix": 0,
 		"no-multi-assign": 0,
 		"no-multi-assign": 0,
-		"no-shadow": 0
+		"no-shadow": 0,
+		"no-new": 0
 	}
 	}
 }
 }

+ 0 - 16
frontend/.snyk

@@ -2,20 +2,4 @@
 version: v1.13.5
 version: v1.13.5
 # ignores vulnerabilities until expiry date; change duration by modifying expiry date
 # ignores vulnerabilities until expiry date; change duration by modifying expiry date
 ignore:
 ignore:
-  'npm:vue:20170401':
-    - vue-roaster > vue:
-        reason: temp
-        expires: '2019-09-04T02:07:16.079Z'
-  'npm:vue:20170829':
-    - vue-roaster > vue:
-        reason: temp
-        expires: '2019-09-04T02:07:16.079Z'
-  'npm:vue:20180222':
-    - vue-roaster > vue:
-        reason: temp
-        expires: '2019-09-04T02:07:16.079Z'
-  'npm:vue:20180802':
-    - vue-roaster > vue:
-        reason: temp
-        expires: '2019-09-04T02:07:16.079Z'
 patch: {}
 patch: {}

+ 17 - 8
frontend/App.vue

@@ -7,7 +7,6 @@
 			</h1>
 			</h1>
 			<!-- should be a persistant toast -->
 			<!-- should be a persistant toast -->
 			<router-view />
 			<router-view />
-			<toast />
 			<what-is-new />
 			<what-is-new />
 			<mobile-alert />
 			<mobile-alert />
 			<login-modal v-if="modals.header.login" />
 			<login-modal v-if="modals.header.login" />
@@ -19,7 +18,7 @@
 <script>
 <script>
 import { mapState, mapActions } from "vuex";
 import { mapState, mapActions } from "vuex";
 
 
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 
 
 import Banned from "./components/pages/Banned.vue";
 import Banned from "./components/pages/Banned.vue";
 import WhatIsNew from "./components/Modals/WhatIsNew.vue";
 import WhatIsNew from "./components/Modals/WhatIsNew.vue";
@@ -65,6 +64,7 @@ export default {
 			this.$router.go(localStorage.getItem("github_redirect"));
 			this.$router.go(localStorage.getItem("github_redirect"));
 			localStorage.removeItem("github_redirect");
 			localStorage.removeItem("github_redirect");
 		}
 		}
+
 		io.onConnect(true, () => {
 		io.onConnect(true, () => {
 			this.socketConnected = true;
 			this.socketConnected = true;
 		});
 		});
@@ -74,9 +74,11 @@ export default {
 		io.onDisconnect(true, () => {
 		io.onDisconnect(true, () => {
 			this.socketConnected = false;
 			this.socketConnected = false;
 		});
 		});
-		lofig.get("serverDomain", res => {
-			this.serverDomain = res;
+
+		lofig.get("serverDomain").then(serverDomain => {
+			this.serverDomain = serverDomain;
 		});
 		});
+
 		this.$router.onReady(() => {
 		this.$router.onReady(() => {
 			if (this.$route.query.err) {
 			if (this.$route.query.err) {
 				let { err } = this.$route.query;
 				let { err } = this.$route.query;
@@ -84,7 +86,7 @@ export default {
 					.replace(new RegExp("<", "g"), "&lt;")
 					.replace(new RegExp("<", "g"), "&lt;")
 					.replace(new RegExp(">", "g"), "&gt;");
 					.replace(new RegExp(">", "g"), "&gt;");
 				this.$router.push({ query: {} });
 				this.$router.push({ query: {} });
-				Toast.methods.addToast(err, 20000);
+				new Toast({ content: err, timeout: 20000 });
 			}
 			}
 			if (this.$route.query.msg) {
 			if (this.$route.query.msg) {
 				let { msg } = this.$route.query;
 				let { msg } = this.$route.query;
@@ -92,7 +94,7 @@ export default {
 					.replace(new RegExp("<", "g"), "&lt;")
 					.replace(new RegExp("<", "g"), "&lt;")
 					.replace(new RegExp(">", "g"), "&gt;");
 					.replace(new RegExp(">", "g"), "&gt;");
 				this.$router.push({ query: {} });
 				this.$router.push({ query: {} });
-				Toast.methods.addToast(msg, 20000);
+				new Toast({ content: msg, timeout: 20000 });
 			}
 			}
 		});
 		});
 		io.getSocket(true, socket => {
 		io.getSocket(true, socket => {
@@ -102,7 +104,6 @@ export default {
 		});
 		});
 	},
 	},
 	components: {
 	components: {
-		Toast,
 		WhatIsNew,
 		WhatIsNew,
 		MobileAlert,
 		MobileAlert,
 		LoginModal,
 		LoginModal,
@@ -115,8 +116,16 @@ export default {
 <style lang="scss">
 <style lang="scss">
 @import "styles/global.scss";
 @import "styles/global.scss";
 
 
-#toast-container {
+#toasts-container {
 	z-index: 10000 !important;
 	z-index: 10000 !important;
+
+	.toast {
+		font-weight: 600;
+	}
+}
+
+.toast:not(:first-of-type) {
+	margin-top: 5px;
 }
 }
 
 
 html {
 html {

+ 5 - 5
frontend/api/auth.js

@@ -1,4 +1,4 @@
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 import io from "../io";
 import io from "../io";
 
 
 // when Vuex needs to interact with socket.io
 // when Vuex needs to interact with socket.io
@@ -18,7 +18,7 @@ export default {
 					res => {
 					res => {
 						if (res.status === "success") {
 						if (res.status === "success") {
 							if (res.SID) {
 							if (res.SID) {
-								return lofig.get("cookie", cookie => {
+								return lofig.get("cookie").then(cookie => {
 									const date = new Date();
 									const date = new Date();
 									date.setTime(
 									date.setTime(
 										new Date().getTime() +
 										new Date().getTime() +
@@ -52,7 +52,7 @@ export default {
 			io.getSocket(socket => {
 			io.getSocket(socket => {
 				socket.emit("users.login", email, password, res => {
 				socket.emit("users.login", email, password, res => {
 					if (res.status === "success") {
 					if (res.status === "success") {
-						return lofig.get("cookie", cookie => {
+						return lofig.get("cookie").then(cookie => {
 							const date = new Date();
 							const date = new Date();
 							date.setTime(
 							date.setTime(
 								new Date().getTime() +
 								new Date().getTime() +
@@ -79,12 +79,12 @@ export default {
 			io.getSocket(socket => {
 			io.getSocket(socket => {
 				socket.emit("users.logout", result => {
 				socket.emit("users.logout", result => {
 					if (result.status === "success") {
 					if (result.status === "success") {
-						return lofig.get("cookie", cookie => {
+						return lofig.get("cookie").then(cookie => {
 							document.cookie = `${cookie.SIDname}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
 							document.cookie = `${cookie.SIDname}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
 							return window.location.reload();
 							return window.location.reload();
 						});
 						});
 					}
 					}
-					Toast.methods.addToast(result.message, 4000);
+					new Toast({ content: result.message, timeout: 4000 });
 					return reject(new Error(result.message));
 					return reject(new Error(result.message));
 				});
 				});
 			});
 			});

+ 0 - 5
frontend/components/404.vue

@@ -12,11 +12,6 @@
 <style lang="scss" scoped>
 <style lang="scss" scoped>
 @import "styles/global.scss";
 @import "styles/global.scss";
 
 
-* {
-	margin: 0;
-	padding: 0;
-}
-
 .wrapper {
 .wrapper {
 	height: 100vh;
 	height: 100vh;
 	display: flex;
 	display: flex;

+ 0 - 501
frontend/components/Admin/EditStation.vue

@@ -1,501 +0,0 @@
-<template>
-	<modal title="Edit Station">
-		<template v-slot:body>
-			<label class="label">Name</label>
-			<p class="control">
-				<input
-					v-model="editing.name"
-					class="input"
-					type="text"
-					placeholder="Station Name"
-				/>
-			</p>
-			<label class="label">Display name</label>
-			<p class="control">
-				<input
-					v-model="editing.displayName"
-					class="input"
-					type="text"
-					placeholder="Station Display Name"
-				/>
-			</p>
-			<label class="label">Description</label>
-			<p class="control">
-				<input
-					v-model="editing.description"
-					class="input"
-					type="text"
-					placeholder="Station Description"
-				/>
-			</p>
-			<label class="label">Privacy</label>
-			<p class="control">
-				<span class="select">
-					<select v-model="editing.privacy">
-						<option value="public">Public</option>
-						<option value="unlisted">Unlisted</option>
-						<option value="private">Private</option>
-					</select>
-				</span>
-			</p>
-			<br />
-			<p class="control" v-if="station.type === 'community'">
-				<label class="checkbox party-mode-inner">
-					<input v-model="editing.partyMode" type="checkbox" />
-					&nbsp;Party mode
-				</label>
-			</p>
-			<small v-if="station.type === 'community'"
-				>With party mode enabled, people can add songs to a queue that
-				plays. With party mode disabled you can play a private playlist
-				on loop.</small
-			>
-			<br />
-			<div v-if="station.type === 'community' && station.partyMode">
-				<br />
-				<br />
-				<label class="label">Queue lock</label>
-				<small v-if="station.partyMode"
-					>With the queue locked, only owners (you) can add songs to
-					the queue.</small
-				>
-				<br />
-				<button
-					v-if="!station.locked"
-					class="button is-danger"
-					@click="$parent.toggleLock()"
-				>
-					Lock the queue
-				</button>
-				<button
-					v-if="station.locked"
-					class="button is-success"
-					@click="$parent.toggleLock()"
-				>
-					Unlock the queue
-				</button>
-			</div>
-			<div
-				v-if="station.type === 'official'"
-				class="control is-grouped genre-wrapper"
-			>
-				<div class="sector">
-					<p class="control has-addons">
-						<input
-							id="new-genre-edit"
-							class="input"
-							type="text"
-							placeholder="Genre"
-							@keyup.enter="addGenre()"
-						/>
-						<a class="button is-info" href="#" @click="addGenre()"
-							>Add genre</a
-						>
-					</p>
-					<span
-						v-for="(genre, index) in editing.genres"
-						:key="index"
-						class="tag is-info"
-					>
-						{{ genre }}
-						<button
-							class="delete is-info"
-							@click="removeGenre(index)"
-						/>
-					</span>
-				</div>
-				<div class="sector">
-					<p class="control has-addons">
-						<input
-							id="new-blacklisted-genre-edit"
-							class="input"
-							type="text"
-							placeholder="Blacklisted Genre"
-							@keyup.enter="addBlacklistedGenre()"
-						/>
-						<a
-							class="button is-info"
-							href="#"
-							@click="addBlacklistedGenre()"
-							>Add blacklisted genre</a
-						>
-					</p>
-					<span
-						v-for="(genre, index) in editing.blacklistedGenres"
-						:key="index"
-						class="tag is-info"
-					>
-						{{ genre }}
-						<button
-							class="delete is-info"
-							@click="removeBlacklistedGenre(index)"
-						/>
-					</span>
-				</div>
-			</div>
-		</template>
-		<template v-slot:footer>
-			<button class="button is-success" v-on:click="update()">
-				Update Settings
-			</button>
-			<button
-				v-if="station.type === 'community'"
-				class="button is-danger"
-				@click="deleteStation()"
-			>
-				Delete station
-			</button>
-		</template>
-	</modal>
-</template>
-
-<script>
-import { mapState, mapActions } from "vuex";
-import { Toast } from "vue-roaster";
-
-import Modal from "../Modals/Modal.vue";
-import io from "../../io";
-import validation from "../../validation";
-
-export default {
-	computed: mapState("admin/stations", {
-		station: state => state.station,
-		editing: state => state.editing
-	}),
-	mounted() {
-		io.getSocket(socket => {
-			this.socket = socket;
-			return socket;
-		});
-	},
-	methods: {
-		update() {
-			if (this.station.name !== this.editing.name) this.updateName();
-			if (this.station.displayName !== this.editing.displayName)
-				this.updateDisplayName();
-			if (this.station.description !== this.editing.description)
-				this.updateDescription();
-			if (this.station.privacy !== this.editing.privacy)
-				this.updatePrivacy();
-			if (this.station.partyMode !== this.editing.partyMode)
-				this.updatePartyMode();
-			if (
-				this.station.genres.toString() !==
-				this.editing.genres.toString()
-			)
-				this.updateGenres();
-			if (
-				this.station.blacklistedGenres.toString() !==
-				this.editing.blacklistedGenres.toString()
-			)
-				this.updateBlacklistedGenres();
-		},
-		updateName() {
-			const { name } = this.editing;
-			if (!validation.isLength(name, 2, 16))
-				return Toast.methods.addToast(
-					"Name must have between 2 and 16 characters.",
-					8000
-				);
-			if (!validation.regex.az09_.test(name))
-				return Toast.methods.addToast(
-					"Invalid name format. Allowed characters: a-z, 0-9 and _.",
-					8000
-				);
-
-			return this.socket.emit(
-				"stations.updateName",
-				this.editing._id,
-				name,
-				res => {
-					if (res.status === "success") {
-						if (this.station) this.station.name = name;
-						this.$parent.stations.forEach((station, index) => {
-							if (station._id === this.editing._id) {
-								this.$parent.stations[index].name = name;
-								return name;
-							}
-
-							return false;
-						});
-					}
-					Toast.methods.addToast(res.message, 8000);
-				}
-			);
-		},
-		updateDisplayName() {
-			const { displayName } = this.editing;
-			if (!validation.isLength(displayName, 2, 32))
-				return Toast.methods.addToast(
-					"Display name must have between 2 and 32 characters.",
-					8000
-				);
-			if (!validation.regex.azAZ09_.test(displayName))
-				return Toast.methods.addToast(
-					"Invalid display name format. Allowed characters: a-z, A-Z, 0-9 and _.",
-					8000
-				);
-
-			return this.socket.emit(
-				"stations.updateDisplayName",
-				this.editing._id,
-				displayName,
-				res => {
-					if (res.status === "success") {
-						if (this.station) {
-							this.station.displayName = displayName;
-							return displayName;
-						}
-						this.$parent.stations.forEach((station, index) => {
-							if (station._id === this.editing._id) {
-								this.$parent.stations[
-									index
-								].displayName = displayName;
-								return displayName;
-							}
-
-							return false;
-						});
-					}
-
-					return Toast.methods.addToast(res.message, 8000);
-				}
-			);
-		},
-		updateDescription() {
-			const { description } = this.editing;
-			if (!validation.isLength(description, 2, 200))
-				return Toast.methods.addToast(
-					"Description must have between 2 and 200 characters.",
-					8000
-				);
-			let characters = description.split("");
-			characters = characters.filter(character => {
-				return character.charCodeAt(0) === 21328;
-			});
-			if (characters.length !== 0)
-				return Toast.methods.addToast(
-					"Invalid description format. Swastika's are not allowed.",
-					8000
-				);
-
-			return this.socket.emit(
-				"stations.updateDescription",
-				this.editing._id,
-				description,
-				res => {
-					if (res.status === "success") {
-						if (this.station) {
-							this.station.description = description;
-							return description;
-						}
-						this.$parent.stations.forEach((station, index) => {
-							if (station._id === this.editing._id) {
-								this.$parent.stations[
-									index
-								].description = description;
-								return description;
-							}
-
-							return false;
-						});
-
-						return Toast.methods.addToast(res.message, 4000);
-					}
-
-					return Toast.methods.addToast(res.message, 8000);
-				}
-			);
-		},
-		updatePrivacy() {
-			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;
-						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;
-								}
-
-								return false;
-							});
-						}
-						return Toast.methods.addToast(res.message, 4000);
-					}
-
-					return Toast.methods.addToast(res.message, 8000);
-				}
-			);
-		},
-		updateGenres() {
-			this.socket.emit(
-				"stations.updateGenres",
-				this.editing._id,
-				this.editing.genres,
-				res => {
-					if (res.status === "success") {
-						const genres = JSON.parse(
-							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;
-								return genres;
-							}
-
-							return false;
-						});
-						return Toast.methods.addToast(res.message, 4000);
-					}
-
-					return Toast.methods.addToast(res.message, 8000);
-				}
-			);
-		},
-		updateBlacklistedGenres() {
-			this.socket.emit(
-				"stations.updateBlacklistedGenres",
-				this.editing._id,
-				this.editing.blacklistedGenres,
-				res => {
-					if (res.status === "success") {
-						const blacklistedGenres = JSON.parse(
-							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[
-									index
-								].blacklistedGenres = blacklistedGenres;
-								return blacklistedGenres;
-							}
-
-							return false;
-						});
-						return Toast.methods.addToast(res.message, 4000);
-					}
-
-					return Toast.methods.addToast(res.message, 8000);
-				}
-			);
-		},
-		updatePartyMode() {
-			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;
-							}
-
-							return false;
-						});
-
-						return Toast.methods.addToast(res.message, 4000);
-					}
-
-					return Toast.methods.addToast(res.message, 8000);
-				}
-			);
-		},
-		addGenre() {
-			const genre = document
-				.getElementById(`new-genre-edit`)
-				.value.toLowerCase()
-				.trim();
-
-			if (this.editing.genres.indexOf(genre) !== -1)
-				return Toast.methods.addToast("Genre already exists", 3000);
-			if (genre) {
-				this.editing.genres.push(genre);
-				document.getElementById(`new-genre`).value = "";
-				return true;
-			}
-			return Toast.methods.addToast("Genre cannot be empty", 3000);
-		},
-		removeGenre(index) {
-			this.editing.genres.splice(index, 1);
-		},
-		addBlacklistedGenre() {
-			const genre = document
-				.getElementById(`new-blacklisted-genre-edit`)
-				.value.toLowerCase()
-				.trim();
-			if (this.editing.blacklistedGenres.indexOf(genre) !== -1)
-				return Toast.methods.addToast("Genre already exists", 3000);
-
-			if (genre) {
-				this.editing.blacklistedGenres.push(genre);
-				document.getElementById(`new-blacklisted-genre`).value = "";
-				return true;
-			}
-			return Toast.methods.addToast("Genre cannot be empty", 3000);
-		},
-		removeBlacklistedGenre(index) {
-			this.editing.blacklistedGenres.splice(index, 1);
-		},
-		deleteStation() {
-			this.socket.emit("stations.remove", this.editing._id, res => {
-				if (res.status === "success")
-					this.closeModal({
-						sector: "station",
-						modal: "editStation"
-					});
-				return Toast.methods.addToast(res.message, 8000);
-			});
-		},
-		...mapActions("modals", ["closeModal"])
-	},
-	components: { Modal }
-};
-</script>
-
-<style lang="scss" scoped>
-@import "styles/global.scss";
-
-.controls {
-	display: flex;
-
-	a {
-		display: flex;
-		align-items: center;
-	}
-}
-
-.table {
-	margin-bottom: 0;
-}
-
-h5 {
-	padding: 20px 0;
-}
-
-.party-mode-inner,
-.party-mode-outer {
-	display: flex;
-	align-items: center;
-}
-
-.select:after {
-	border-color: $primary-color;
-}
-</style>

+ 26 - 18
frontend/components/Admin/News.vue

@@ -215,7 +215,7 @@
 <script>
 <script>
 import { mapActions, mapState } from "vuex";
 import { mapActions, mapState } from "vuex";
 
 
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 import io from "../../io";
 import io from "../../io";
 
 
 import EditNews from "../Modals/EditNews.vue";
 import EditNews from "../Modals/EditNews.vue";
@@ -267,28 +267,28 @@ export default {
 			} = this;
 			} = this;
 
 
 			if (this.creating.title === "")
 			if (this.creating.title === "")
-				return Toast.methods.addToast(
-					"Field (Title) cannot be empty",
-					3000
-				);
+				return new Toast({
+					content: "Field (Title) cannot be empty",
+					timeout: 3000
+				});
 			if (this.creating.description === "")
 			if (this.creating.description === "")
-				return Toast.methods.addToast(
-					"Field (Description) cannot be empty",
-					3000
-				);
+				return new Toast({
+					content: "Field (Description) cannot be empty",
+					timeout: 3000
+				});
 			if (
 			if (
 				bugs.length <= 0 &&
 				bugs.length <= 0 &&
 				features.length <= 0 &&
 				features.length <= 0 &&
 				improvements.length <= 0 &&
 				improvements.length <= 0 &&
 				upcoming.length <= 0
 				upcoming.length <= 0
 			)
 			)
-				return Toast.methods.addToast(
-					"You must have at least one News Item",
-					3000
-				);
+				return new Toast({
+					content: "You must have at least one News Item",
+					timeout: 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);
+				new Toast(result.message, 4000);
 				if (result.status === "success")
 				if (result.status === "success")
 					this.creating = {
 					this.creating = {
 						title: "",
 						title: "",
@@ -301,8 +301,10 @@ export default {
 			});
 			});
 		},
 		},
 		removeNews(news) {
 		removeNews(news) {
-			this.socket.emit("news.remove", news, res =>
-				Toast.methods.addToast(res.message, 8000)
+			this.socket.emit(
+				"news.remove",
+				news,
+				res => new Toast({ content: res.message, timeout: 8000 })
 			);
 			);
 		},
 		},
 		editNewsClick(news) {
 		editNewsClick(news) {
@@ -313,14 +315,20 @@ export default {
 			const change = document.getElementById(`new-${type}`).value.trim();
 			const change = document.getElementById(`new-${type}`).value.trim();
 
 
 			if (this.creating[type].indexOf(change) !== -1)
 			if (this.creating[type].indexOf(change) !== -1)
-				return Toast.methods.addToast(`Tag already exists`, 3000);
+				return new Toast({
+					content: `Tag already exists`,
+					timeout: 3000
+				});
 
 
 			if (change) {
 			if (change) {
 				document.getElementById(`new-${type}`).value = "";
 				document.getElementById(`new-${type}`).value = "";
 				this.creating[type].push(change);
 				this.creating[type].push(change);
 				return true;
 				return true;
 			}
 			}
-			return Toast.methods.addToast(`${type} cannot be empty`, 3000);
+			return new Toast({
+				content: `${type} cannot be empty`,
+				timeout: 3000
+			});
 		},
 		},
 		removeChange(type, index) {
 		removeChange(type, index) {
 			this.creating[type].splice(index, 1);
 			this.creating[type].splice(index, 1);

+ 2 - 2
frontend/components/Admin/Punishments.vue

@@ -113,7 +113,7 @@
 
 
 <script>
 <script>
 import { mapState, mapActions } from "vuex";
 import { mapState, mapActions } from "vuex";
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 
 
 import ViewPunishment from "../Modals/ViewPunishment.vue";
 import ViewPunishment from "../Modals/ViewPunishment.vue";
 import io from "../../io";
 import io from "../../io";
@@ -149,7 +149,7 @@ export default {
 				this.ipBan.reason,
 				this.ipBan.reason,
 				this.ipBan.expiresAt,
 				this.ipBan.expiresAt,
 				res => {
 				res => {
-					Toast.methods.addToast(res.message, 6000);
+					new Toast({ content: res.message, timeout: 6000 });
 				}
 				}
 			);
 			);
 		},
 		},

+ 53 - 34
frontend/components/Admin/QueueSongs.vue

@@ -1,13 +1,25 @@
 <template>
 <template>
 	<div>
 	<div>
 		<metadata title="Admin | Queue songs" />
 		<metadata title="Admin | Queue songs" />
-		<div class="container">
+		<div class="container" v-scroll="handleScroll">
+			<p>
+				<span>Sets loaded: {{ position - 1 }} / {{ maxPosition }}</span>
+				<br />
+				<span>Loaded songs: {{ this.songs.length }}</span>
+			</p>
 			<input
 			<input
 				v-model="searchQuery"
 				v-model="searchQuery"
 				type="text"
 				type="text"
 				class="input"
 				class="input"
 				placeholder="Search for Songs"
 				placeholder="Search for Songs"
 			/>
 			/>
+			<button
+				v-if="!loadAllSongs"
+				class="button is-primary"
+				@click="loadAll()"
+			>
+				Load all
+			</button>
 			<br />
 			<br />
 			<br />
 			<br />
 			<table class="table is-striped">
 			<table class="table is-striped">
@@ -79,24 +91,6 @@
 				</tbody>
 				</tbody>
 			</table>
 			</table>
 		</div>
 		</div>
-		<nav class="pagination">
-			<a
-				v-if="position > 1"
-				class="button"
-				href="#"
-				@click="getSet(position - 1)"
-			>
-				<i class="material-icons">navigate_before</i>
-			</a>
-			<a
-				v-if="maxPosition > position"
-				class="button"
-				href="#"
-				@click="getSet(position + 1)"
-			>
-				<i class="material-icons">navigate_next</i>
-			</a>
-		</nav>
 		<edit-song v-if="modals.editSong" />
 		<edit-song v-if="modals.editSong" />
 	</div>
 	</div>
 </template>
 </template>
@@ -104,7 +98,7 @@
 <script>
 <script>
 import { mapState, mapActions } from "vuex";
 import { mapState, mapActions } from "vuex";
 
 
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 
 
 import EditSong from "../Modals/EditSong.vue";
 import EditSong from "../Modals/EditSong.vue";
 import UserIdToUsername from "../UserIdToUsername.vue";
 import UserIdToUsername from "../UserIdToUsername.vue";
@@ -118,7 +112,9 @@ export default {
 			position: 1,
 			position: 1,
 			maxPosition: 1,
 			maxPosition: 1,
 			searchQuery: "",
 			searchQuery: "",
-			songs: []
+			songs: [],
+			gettingSet: false,
+			loadAllSongs: false
 		};
 		};
 	},
 	},
 	computed: {
 	computed: {
@@ -141,12 +137,6 @@ export default {
 	//   }
 	//   }
 	// },
 	// },
 	methods: {
 	methods: {
-		getSet(position) {
-			this.socket.emit("queueSongs.getSet", position, data => {
-				this.songs = data;
-				this.position = position;
-			});
-		},
 		edit(song, index) {
 		edit(song, index) {
 			const newSong = {};
 			const newSong = {};
 			Object.keys(song).forEach(n => {
 			Object.keys(song).forEach(n => {
@@ -159,21 +149,50 @@ export default {
 		add(song) {
 		add(song) {
 			this.socket.emit("songs.add", song, res => {
 			this.socket.emit("songs.add", song, res => {
 				if (res.status === "success")
 				if (res.status === "success")
-					Toast.methods.addToast(res.message, 2000);
-				else Toast.methods.addToast(res.message, 4000);
+					new Toast({ content: res.message, timeout: 2000 });
+				else new Toast({ content: res.message, timeout: 4000 });
 			});
 			});
 		},
 		},
 		remove(id) {
 		remove(id) {
 			this.socket.emit("queueSongs.remove", id, res => {
 			this.socket.emit("queueSongs.remove", id, res => {
 				if (res.status === "success")
 				if (res.status === "success")
-					Toast.methods.addToast(res.message, 2000);
-				else Toast.methods.addToast(res.message, 4000);
+					new Toast({ content: res.message, timeout: 2000 });
+				else new Toast({ content: res.message, timeout: 4000 });
+			});
+		},
+		getSet() {
+			if (this.gettingSet) return;
+			if (this.position > this.maxPosition) return;
+			this.gettingSet = true;
+			this.socket.emit("queueSongs.getSet", this.position, data => {
+				data.forEach(song => {
+					this.songs.push(song);
+				});
+				this.position += 1;
+				this.gettingSet = false;
+				if (this.loadAllSongs && this.maxPosition > this.position - 1)
+					setTimeout(() => {
+						this.getSet();
+					}, 500);
 			});
 			});
 		},
 		},
+		handleScroll() {
+			if (this.loadAllSongs) return false;
+			if (window.scrollY + 50 >= window.scrollMaxY) this.getSet();
+
+			return this.maxPosition === this.position;
+		},
+		loadAll() {
+			this.loadAllSongs = true;
+			this.getSet();
+		},
 		init() {
 		init() {
-			this.socket.emit("queueSongs.index", data => {
-				this.songs = data.songs;
-				this.maxPosition = Math.round(data.maxLength / 50);
+			if (this.songs.length > 0)
+				this.position = Math.ceil(this.songs.length / 15) + 1;
+
+			this.socket.emit("queueSongs.length", length => {
+				this.maxPosition = Math.ceil(length / 15);
+				this.getSet();
 			});
 			});
 			this.socket.emit("apis.joinAdminRoom", "queue", () => {});
 			this.socket.emit("apis.joinAdminRoom", "queue", () => {});
 		},
 		},

+ 21 - 9
frontend/components/Admin/Reports.vue

@@ -6,8 +6,8 @@
 				<thead>
 				<thead>
 					<tr>
 					<tr>
 						<td>Song ID</td>
 						<td>Song ID</td>
-						<td>Created By</td>
-						<td>Created At</td>
+						<td>Author</td>
+						<td>Time of report</td>
 						<td>Description</td>
 						<td>Description</td>
 						<td>Options</td>
 						<td>Options</td>
 					</tr>
 					</tr>
@@ -28,7 +28,13 @@
 							/>
 							/>
 						</td>
 						</td>
 						<td>
 						<td>
-							<span>{{ report.createdAt }}</span>
+							<span :title="report.createdAt">{{
+								formatDistance(
+									new Date(report.createdAt),
+									new Date(),
+									{ addSuffix: true }
+								)
+							}}</span>
 						</td>
 						</td>
 						<td>
 						<td>
 							<span>{{ report.description }}</span>
 							<span>{{ report.description }}</span>
@@ -58,8 +64,9 @@
 
 
 <script>
 <script>
 import { mapState, mapActions } from "vuex";
 import { mapState, mapActions } from "vuex";
+import { formatDistance } from "date-fns";
 
 
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 import io from "../../io";
 import io from "../../io";
 
 
 import IssuesModal from "../Modals/IssuesModal.vue";
 import IssuesModal from "../Modals/IssuesModal.vue";
@@ -76,17 +83,21 @@ export default {
 		io.getSocket(socket => {
 		io.getSocket(socket => {
 			this.socket = socket;
 			this.socket = socket;
 			if (this.socket.connected) this.init();
 			if (this.socket.connected) this.init();
+
 			this.socket.emit("reports.index", res => {
 			this.socket.emit("reports.index", res => {
 				this.reports = res.data;
 				this.reports = res.data;
 			});
 			});
+
 			this.socket.on("event:admin.report.resolved", reportId => {
 			this.socket.on("event:admin.report.resolved", reportId => {
 				this.reports = this.reports.filter(report => {
 				this.reports = this.reports.filter(report => {
 					return report._id !== reportId;
 					return report._id !== reportId;
 				});
 				});
 			});
 			});
+
 			this.socket.on("event:admin.report.created", report => {
 			this.socket.on("event:admin.report.created", report => {
 				this.reports.push(report);
 				this.reports.push(report);
 			});
 			});
+
 			io.onConnect(() => {
 			io.onConnect(() => {
 				this.init();
 				this.init();
 			});
 			});
@@ -96,10 +107,10 @@ export default {
 			this.socket.emit("reports.findOne", this.$route.query.id, res => {
 			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
 				else
-					Toast.methods.addToast(
-						"Report with that ID not found",
-						3000
-					);
+					new Toast({
+						content: "Report with that ID not found",
+						timeout: 3000
+					});
 			});
 			});
 		}
 		}
 	},
 	},
@@ -109,6 +120,7 @@ export default {
 		})
 		})
 	},
 	},
 	methods: {
 	methods: {
+		formatDistance,
 		init() {
 		init() {
 			this.socket.emit("apis.joinAdminRoom", "reports", () => {});
 			this.socket.emit("apis.joinAdminRoom", "reports", () => {});
 		},
 		},
@@ -118,7 +130,7 @@ export default {
 		},
 		},
 		resolve(reportId) {
 		resolve(reportId) {
 			this.socket.emit("reports.resolve", reportId, res => {
 			this.socket.emit("reports.resolve", reportId, res => {
-				Toast.methods.addToast(res.message, 3000);
+				new Toast({ content: res.message, timeout: 3000 });
 				if (res.status === "success" && this.modals.viewReport)
 				if (res.status === "success" && this.modals.viewReport)
 					this.closeModal({
 					this.closeModal({
 						sector: "admin",
 						sector: "admin",

+ 46 - 9
frontend/components/Admin/Songs.vue

@@ -1,13 +1,25 @@
 <template>
 <template>
 	<div>
 	<div>
 		<metadata title="Admin | Songs" />
 		<metadata title="Admin | Songs" />
-		<div class="container">
+		<div class="container" v-scroll="handleScroll">
+			<p>
+				<span>Sets loaded: {{ position - 1 }} / {{ maxPosition }}</span>
+				<br />
+				<span>Loaded songs: {{ this.songs.length }}</span>
+			</p>
 			<input
 			<input
 				v-model="searchQuery"
 				v-model="searchQuery"
 				type="text"
 				type="text"
 				class="input"
 				class="input"
 				placeholder="Search for Songs"
 				placeholder="Search for Songs"
 			/>
 			/>
+			<button
+				v-if="!loadAllSongs"
+				class="button is-primary"
+				@click="loadAll()"
+			>
+				Load all
+			</button>
 			<br />
 			<br />
 			<br />
 			<br />
 			<table class="table is-striped">
 			<table class="table is-striped">
@@ -90,7 +102,7 @@
 <script>
 <script>
 import { mapState, mapActions } from "vuex";
 import { mapState, mapActions } from "vuex";
 
 
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 
 
 import EditSong from "../Modals/EditSong.vue";
 import EditSong from "../Modals/EditSong.vue";
 import UserIdToUsername from "../UserIdToUsername.vue";
 import UserIdToUsername from "../UserIdToUsername.vue";
@@ -107,7 +119,9 @@ export default {
 			editing: {
 			editing: {
 				index: 0,
 				index: 0,
 				song: {}
 				song: {}
-			}
+			},
+			gettingSet: false,
+			loadAllSongs: false
 		};
 		};
 	},
 	},
 	computed: {
 	computed: {
@@ -139,20 +153,40 @@ export default {
 		remove(id) {
 		remove(id) {
 			this.socket.emit("songs.remove", id, res => {
 			this.socket.emit("songs.remove", id, res => {
 				if (res.status === "success")
 				if (res.status === "success")
-					Toast.methods.addToast(res.message, 4000);
-				else Toast.methods.addToast(res.message, 8000);
+					new Toast({ content: res.message, timeout: 4000 });
+				else new Toast({ content: res.message, timeout: 8000 });
 			});
 			});
 		},
 		},
 		getSet() {
 		getSet() {
+			if (this.gettingSet) return;
+			if (this.position > this.maxPosition) return;
+			this.gettingSet = true;
 			this.socket.emit("songs.getSet", this.position, data => {
 			this.socket.emit("songs.getSet", this.position, data => {
 				data.forEach(song => {
 				data.forEach(song => {
 					this.addSong(song);
 					this.addSong(song);
 				});
 				});
 				this.position += 1;
 				this.position += 1;
-				if (this.maxPosition > this.position - 1) this.getSet();
+				this.gettingSet = false;
+				if (this.loadAllSongs && this.maxPosition > this.position - 1)
+					setTimeout(() => {
+						this.getSet();
+					}, 500);
 			});
 			});
 		},
 		},
+		handleScroll() {
+			if (this.loadAllSongs) return false;
+			if (window.scrollY + 50 >= window.scrollMaxY) this.getSet();
+
+			return this.maxPosition === this.position;
+		},
+		loadAll() {
+			this.loadAllSongs = true;
+			this.getSet();
+		},
 		init() {
 		init() {
+			if (this.songs.length > 0)
+				this.position = Math.ceil(this.songs.length / 15) + 1;
+
 			this.socket.emit("songs.length", length => {
 			this.socket.emit("songs.length", length => {
 				this.maxPosition = Math.ceil(length / 15);
 				this.maxPosition = Math.ceil(length / 15);
 				this.getSet();
 				this.getSet();
@@ -189,13 +223,16 @@ export default {
 			});
 			});
 		});
 		});
 
 
-		if (this.$route.query.id) {
-			this.socket.emit("songs.getSong", this.$route.query.id, res => {
+		if (this.$route.query.songId) {
+			this.socket.emit("songs.getSong", this.$route.query.songId, res => {
 				if (res.status === "success") {
 				if (res.status === "success") {
 					this.edit(res.data);
 					this.edit(res.data);
 					this.closeModal({ sector: "admin", modal: "viewReport" });
 					this.closeModal({ sector: "admin", modal: "viewReport" });
 				} else
 				} else
-					Toast.methods.addToast("Song with that ID not found", 3000);
+					new Toast({
+						content: "Song with that ID not found",
+						timeout: 3000
+					});
 			});
 			});
 		}
 		}
 	}
 	}

+ 33 - 21
frontend/components/Admin/Stations.vue

@@ -177,17 +177,17 @@
 			</div>
 			</div>
 		</div>
 		</div>
 
 
-		<edit-station v-if="modals.editStation" />
+		<edit-station v-if="modals.editStation" store="admin/stations" />
 	</div>
 	</div>
 </template>
 </template>
 
 
 <script>
 <script>
 import { mapState, mapActions } from "vuex";
 import { mapState, mapActions } from "vuex";
 
 
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 import io from "../../io";
 import io from "../../io";
 
 
-import EditStation from "./EditStation.vue";
+import EditStation from "../Modals/EditStation.vue";
 import UserIdToUsername from "../UserIdToUsername.vue";
 import UserIdToUsername from "../UserIdToUsername.vue";
 
 
 export default {
 export default {
@@ -219,20 +219,20 @@ export default {
 			} = this;
 			} = this;
 
 
 			if (name === undefined)
 			if (name === undefined)
-				return Toast.methods.addToast(
-					"Field (Name) cannot be empty",
-					3000
-				);
+				return new Toast({
+					content: "Field (Name) cannot be empty",
+					timeout: 3000
+				});
 			if (displayName === undefined)
 			if (displayName === undefined)
-				return Toast.methods.addToast(
-					"Field (Display Name) cannot be empty",
-					3000
-				);
+				return new Toast({
+					content: "Field (Display Name) cannot be empty",
+					timeout: 3000
+				});
 			if (description === undefined)
 			if (description === undefined)
-				return Toast.methods.addToast(
-					"Field (Description) cannot be empty",
-					3000
-				);
+				return new Toast({
+					content: "Field (Description) cannot be empty",
+					timeout: 3000
+				});
 
 
 			return this.socket.emit(
 			return this.socket.emit(
 				"stations.create",
 				"stations.create",
@@ -245,7 +245,7 @@ export default {
 					blacklistedGenres
 					blacklistedGenres
 				},
 				},
 				result => {
 				result => {
-					Toast.methods.addToast(result.message, 3000);
+					new Toast({ content: result.message, timeout: 3000 });
 					if (result.status === "success")
 					if (result.status === "success")
 						this.newStation = {
 						this.newStation = {
 							genres: [],
 							genres: [],
@@ -259,7 +259,7 @@ export default {
 				"stations.remove",
 				"stations.remove",
 				this.stations[index]._id,
 				this.stations[index]._id,
 				res => {
 				res => {
-					Toast.methods.addToast(res.message, 3000);
+					new Toast({ content: res.message, timeout: 3000 });
 				}
 				}
 			);
 			);
 		},
 		},
@@ -286,13 +286,19 @@ export default {
 				.value.toLowerCase()
 				.value.toLowerCase()
 				.trim();
 				.trim();
 			if (this.newStation.genres.indexOf(genre) !== -1)
 			if (this.newStation.genres.indexOf(genre) !== -1)
-				return Toast.methods.addToast("Genre already exists", 3000);
+				return new Toast({
+					content: "Genre already exists",
+					timeout: 3000
+				});
 			if (genre) {
 			if (genre) {
 				this.newStation.genres.push(genre);
 				this.newStation.genres.push(genre);
 				document.getElementById(`new-genre`).value = "";
 				document.getElementById(`new-genre`).value = "";
 				return true;
 				return true;
 			}
 			}
-			return Toast.methods.addToast("Genre cannot be empty", 3000);
+			return new Toast({
+				content: "Genre cannot be empty",
+				timeout: 3000
+			});
 		},
 		},
 		removeGenre(index) {
 		removeGenre(index) {
 			this.newStation.genres.splice(index, 1);
 			this.newStation.genres.splice(index, 1);
@@ -303,14 +309,20 @@ export default {
 				.value.toLowerCase()
 				.value.toLowerCase()
 				.trim();
 				.trim();
 			if (this.newStation.blacklistedGenres.indexOf(genre) !== -1)
 			if (this.newStation.blacklistedGenres.indexOf(genre) !== -1)
-				return Toast.methods.addToast("Genre already exists", 3000);
+				return new Toast({
+					content: "Genre already exists",
+					timeout: 3000
+				});
 
 
 			if (genre) {
 			if (genre) {
 				this.newStation.blacklistedGenres.push(genre);
 				this.newStation.blacklistedGenres.push(genre);
 				document.getElementById(`new-blacklisted-genre`).value = "";
 				document.getElementById(`new-blacklisted-genre`).value = "";
 				return true;
 				return true;
 			}
 			}
-			return Toast.methods.addToast("Genre cannot be empty", 3000);
+			return new Toast({
+				content: "Genre cannot be empty",
+				timeout: 3000
+			});
 		},
 		},
 		removeBlacklistedGenre(index) {
 		removeBlacklistedGenre(index) {
 			this.newStation.blacklistedGenres.splice(index, 1);
 			this.newStation.blacklistedGenres.splice(index, 1);

+ 3 - 3
frontend/components/MainFooter.vue

@@ -39,7 +39,7 @@
 				<a href="/"
 				<a href="/"
 					><img
 					><img
 						class="musareFooterLogo"
 						class="musareFooterLogo"
-						src="/assets/wordmark.png"
+						src="/assets/blue_wordmark.png"
 						alt="Musare"
 						alt="Musare"
 				/></a>
 				/></a>
 				<p class="footerLinks">
 				<p class="footerLinks">
@@ -74,8 +74,8 @@ export default {
 		};
 		};
 	},
 	},
 	mounted() {
 	mounted() {
-		lofig.get("siteSettings.socialLinks", res => {
-			this.socialLinks = res;
+		lofig.get("siteSettings.socialLinks").then(socialLinks => {
+			this.socialLinks = socialLinks;
 		});
 		});
 	}
 	}
 };
 };

+ 7 - 9
frontend/components/MainHeader.vue

@@ -3,7 +3,7 @@
 		<div class="nav-left">
 		<div class="nav-left">
 			<router-link class="nav-item is-brand" to="/">
 			<router-link class="nav-item is-brand" to="/">
 				<img
 				<img
-					:src="`${this.siteSettings.logo}`"
+					:src="`${this.siteSettings.logo_white}`"
 					:alt="`${this.siteSettings.siteName}` || `Musare`"
 					:alt="`${this.siteSettings.siteName}` || `Musare`"
 				/>
 				/>
 			</router-link>
 			</router-link>
@@ -85,13 +85,12 @@ export default {
 		};
 		};
 	},
 	},
 	mounted() {
 	mounted() {
-		lofig.get("frontendDomain", res => {
-			this.frontendDomain = res;
-			return res;
+		lofig.get("frontendDomain").then(frontendDomain => {
+			this.frontendDomain = frontendDomain;
 		});
 		});
-		lofig.get("siteSettings", res => {
-			this.siteSettings = res;
-			return res;
+
+		lofig.get("siteSettings").then(siteSettings => {
+			this.siteSettings = siteSettings;
 		});
 		});
 	},
 	},
 	computed: mapState({
 	computed: mapState({
@@ -143,12 +142,11 @@ export default {
 		font-size: 2.1rem !important;
 		font-size: 2.1rem !important;
 		line-height: 38px !important;
 		line-height: 38px !important;
 		padding: 0 20px;
 		padding: 0 20px;
-		color: $white;
 		font-family: Pacifico, cursive;
 		font-family: Pacifico, cursive;
-		filter: brightness(0) invert(1);
 
 
 		img {
 		img {
 			max-height: 38px;
 			max-height: 38px;
+			color: $musareBlue;
 		}
 		}
 	}
 	}
 
 

+ 3 - 3
frontend/components/Modals/AddSongToPlaylist.vue

@@ -40,7 +40,7 @@
 <script>
 <script>
 import { mapState } from "vuex";
 import { mapState } from "vuex";
 
 
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 import Modal from "./Modal.vue";
 import Modal from "./Modal.vue";
 import io from "../../io";
 import io from "../../io";
 
 
@@ -81,7 +81,7 @@ export default {
 				this.currentSong.songId,
 				this.currentSong.songId,
 				playlistId,
 				playlistId,
 				res => {
 				res => {
-					Toast.methods.addToast(res.message, 4000);
+					new Toast({ content: res.message, timeout: 4000 });
 					if (res.status === "success") {
 					if (res.status === "success") {
 						this.playlists[playlistId].songs.push(this.song);
 						this.playlists[playlistId].songs.push(this.song);
 					}
 					}
@@ -95,7 +95,7 @@ export default {
 				this.songId,
 				this.songId,
 				playlistId,
 				playlistId,
 				res => {
 				res => {
-					Toast.methods.addToast(res.message, 4000);
+					new Toast({ content: res.message, timeout: 4000 });
 					if (res.status === "success") {
 					if (res.status === "success") {
 						this.playlists[playlistId].songs.forEach(
 						this.playlists[playlistId].songs.forEach(
 							(song, index) => {
 							(song, index) => {

+ 25 - 13
frontend/components/Modals/AddSongToQueue.vue

@@ -94,7 +94,7 @@
 <script>
 <script>
 import { mapState, mapActions } from "vuex";
 import { mapState, mapActions } from "vuex";
 
 
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 import Modal from "./Modal.vue";
 import Modal from "./Modal.vue";
 import io from "../../io";
 import io from "../../io";
 
 
@@ -138,31 +138,43 @@ export default {
 					songId,
 					songId,
 					data => {
 					data => {
 						if (data.status !== "success")
 						if (data.status !== "success")
-							Toast.methods.addToast(
-								`Error: ${data.message}`,
-								8000
-							);
-						else Toast.methods.addToast(`${data.message}`, 4000);
+							new Toast({
+								content: `Error: ${data.message}`,
+								timeout: 8000
+							});
+						else
+							new Toast({
+								content: `${data.message}`,
+								timeout: 4000
+							});
 					}
 					}
 				);
 				);
 			} else {
 			} else {
 				this.socket.emit("queueSongs.add", songId, data => {
 				this.socket.emit("queueSongs.add", songId, data => {
 					if (data.status !== "success")
 					if (data.status !== "success")
-						Toast.methods.addToast(`Error: ${data.message}`, 8000);
-					else Toast.methods.addToast(`${data.message}`, 4000);
+						new Toast({
+							content: `Error: ${data.message}`,
+							timeout: 8000
+						});
+					else
+						new Toast({
+							content: `${data.message}`,
+							timeout: 4000
+						});
 				});
 				});
 			}
 			}
 		},
 		},
 		importPlaylist() {
 		importPlaylist() {
-			Toast.methods.addToast(
-				"Starting to import your playlist. This can take some time to do.",
-				4000
-			);
+			new Toast({
+				content:
+					"Starting to import your playlist. This can take some time to do.",
+				timeout: 4000
+			});
 			this.socket.emit(
 			this.socket.emit(
 				"queueSongs.addSetToQueue",
 				"queueSongs.addSetToQueue",
 				this.importQuery,
 				this.importQuery,
 				res => {
 				res => {
-					Toast.methods.addToast(res.message, 4000);
+					new Toast({ content: res.message, timeout: 4000 });
 				}
 				}
 			);
 			);
 		},
 		},

+ 39 - 35
frontend/components/Modals/CreateCommunityStation.vue

@@ -41,7 +41,7 @@
 <script>
 <script>
 import { mapActions } from "vuex";
 import { mapActions } from "vuex";
 
 
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 import Modal from "./Modal.vue";
 import Modal from "./Modal.vue";
 import io from "../../io";
 import io from "../../io";
 import validation from "../../validation";
 import validation from "../../validation";
@@ -67,39 +67,43 @@ export default {
 			const { name, displayName, description } = this.newCommunity;
 			const { name, displayName, description } = this.newCommunity;
 
 
 			if (!name || !displayName || !description)
 			if (!name || !displayName || !description)
-				return Toast.methods.addToast(
-					"Please fill in all fields",
-					8000
-				);
+				return new Toast({
+					content: "Please fill in all fields",
+					timeout: 8000
+				});
 
 
 			if (!validation.isLength(name, 2, 16))
 			if (!validation.isLength(name, 2, 16))
-				return Toast.methods.addToast(
-					"Name must have between 2 and 16 characters.",
-					8000
-				);
+				return new Toast({
+					content: "Name must have between 2 and 16 characters.",
+					timeout: 8000
+				});
 
 
 			if (!validation.regex.az09_.test(name))
 			if (!validation.regex.az09_.test(name))
-				return Toast.methods.addToast(
-					"Invalid name format. Allowed characters: a-z, 0-9 and _.",
-					8000
-				);
+				return new Toast({
+					content:
+						"Invalid name format. Allowed characters: a-z, 0-9 and _.",
+					timeout: 8000
+				});
 
 
 			if (!validation.isLength(displayName, 2, 32))
 			if (!validation.isLength(displayName, 2, 32))
-				return Toast.methods.addToast(
-					"Display name must have between 2 and 32 characters.",
-					8000
-				);
-			if (!validation.regex.azAZ09_.test(displayName))
-				return Toast.methods.addToast(
-					"Invalid display name format. Allowed characters: a-z, A-Z, 0-9 and _.",
-					8000
-				);
+				return new Toast({
+					content:
+						"Display name must have between 2 and 32 characters.",
+					timeout: 8000
+				});
+			if (!validation.regex.ascii.test(displayName))
+				return new Toast({
+					content:
+						"Invalid display name format. Only ASCII characters are allowed.",
+					timeout: 8000
+				});
 
 
 			if (!validation.isLength(description, 2, 200))
 			if (!validation.isLength(description, 2, 200))
-				return Toast.methods.addToast(
-					"Description must have between 2 and 200 characters.",
-					8000
-				);
+				return new Toast({
+					content:
+						"Description must have between 2 and 200 characters.",
+					timeout: 8000
+				});
 
 
 			let characters = description.split("");
 			let characters = description.split("");
 
 
@@ -108,10 +112,10 @@ export default {
 			});
 			});
 
 
 			if (characters.length !== 0)
 			if (characters.length !== 0)
-				return Toast.methods.addToast(
-					"Invalid description format. Swastika's are not allowed.",
-					8000
-				);
+				return new Toast({
+					content: "Invalid description format.",
+					timeout: 8000
+				});
 
 
 			return this.socket.emit(
 			return this.socket.emit(
 				"stations.create",
 				"stations.create",
@@ -123,15 +127,15 @@ export default {
 				},
 				},
 				res => {
 				res => {
 					if (res.status === "success") {
 					if (res.status === "success") {
-						Toast.methods.addToast(
-							`You have added the station successfully`,
-							4000
-						);
+						new Toast({
+							content: `You have added the station successfully`,
+							timeout: 4000
+						});
 						this.closeModal({
 						this.closeModal({
 							sector: "home",
 							sector: "home",
 							modal: "createCommunityStation"
 							modal: "createCommunityStation"
 						});
 						});
-					} else Toast.methods.addToast(res.message, 4000);
+					} else new Toast({ content: res.message, timeout: 4000 });
 				}
 				}
 			);
 			);
 		},
 		},

+ 11 - 4
frontend/components/Modals/EditNews.vue

@@ -169,7 +169,7 @@
 <script>
 <script>
 import { mapActions, mapState } from "vuex";
 import { mapActions, mapState } from "vuex";
 
 
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 import io from "../../io";
 import io from "../../io";
 
 
 import Modal from "./Modal.vue";
 import Modal from "./Modal.vue";
@@ -186,10 +186,17 @@ export default {
 			const change = document.getElementById(`edit-${type}`).value.trim();
 			const change = document.getElementById(`edit-${type}`).value.trim();
 
 
 			if (this.editing[type].indexOf(change) !== -1)
 			if (this.editing[type].indexOf(change) !== -1)
-				return Toast.methods.addToast(`Tag already exists`, 3000);
+				return new Toast({
+					content: `Tag already exists`,
+					timeout: 3000
+				});
 
 
 			if (change) this.addChange({ type, change });
 			if (change) this.addChange({ type, change });
-			else Toast.methods.addToast(`${type} cannot be empty`, 3000);
+			else
+				new Toast({
+					content: `${type} cannot be empty`,
+					timeout: 3000
+				});
 
 
 			document.getElementById(`edit-${type}`).value = "";
 			document.getElementById(`edit-${type}`).value = "";
 			return true;
 			return true;
@@ -203,7 +210,7 @@ export default {
 				this.editing._id,
 				this.editing._id,
 				this.editing,
 				this.editing,
 				res => {
 				res => {
-					Toast.methods.addToast(res.message, 4000);
+					new Toast({ content: res.message, timeout: 4000 });
 					if (res.status === "success") {
 					if (res.status === "success") {
 						if (close)
 						if (close)
 							this.closeModal({
 							this.closeModal({

+ 139 - 98
frontend/components/Modals/EditSong.vue

@@ -84,20 +84,24 @@
 										class="button album-get-button"
 										class="button album-get-button"
 										v-on:click="getAlbumData('title')"
 										v-on:click="getAlbumData('title')"
 									>
 									>
-										<i class="material-icons album-get-icon"
-											>album</i
-										>
+										<i class="material-icons">album</i>
 									</button>
 									</button>
 								</p>
 								</p>
 							</div>
 							</div>
 							<div class="duration-container">
 							<div class="duration-container">
 								<label class="label">Duration</label>
 								<label class="label">Duration</label>
-								<p class="control">
+								<p class="control has-addons">
 									<input
 									<input
 										class="input"
 										class="input"
 										type="text"
 										type="text"
-										v-model="editing.song.duration"
+										v-model.number="editing.song.duration"
 									/>
 									/>
+									<button
+										class="button duration-fill-button"
+										v-on:click="fillDuration()"
+									>
+										<i class="material-icons">sync</i>
+									</button>
 								</p>
 								</p>
 							</div>
 							</div>
 							<div class="skip-duration-container">
 							<div class="skip-duration-container">
@@ -106,7 +110,9 @@
 									<input
 									<input
 										class="input"
 										class="input"
 										type="text"
 										type="text"
-										v-model="editing.song.skipDuration"
+										v-model.number="
+											editing.song.skipDuration
+										"
 									/>
 									/>
 								</p>
 								</p>
 							</div>
 							</div>
@@ -124,9 +130,7 @@
 										class="button album-get-button"
 										class="button album-get-button"
 										v-on:click="getAlbumData('albumArt')"
 										v-on:click="getAlbumData('albumArt')"
 									>
 									>
-										<i class="material-icons album-get-icon"
-											>album</i
-										>
+										<i class="material-icons">album</i>
 									</button>
 									</button>
 								</p>
 								</p>
 							</div>
 							</div>
@@ -148,9 +152,7 @@
 										class="button album-get-button"
 										class="button album-get-button"
 										v-on:click="getAlbumData('artists')"
 										v-on:click="getAlbumData('artists')"
 									>
 									>
-										<i class="material-icons album-get-icon"
-											>album</i
-										>
+										<i class="material-icons">album</i>
 									</button>
 									</button>
 									<button
 									<button
 										class="button is-info add-button"
 										class="button is-info add-button"
@@ -224,9 +226,7 @@
 										class="button album-get-button"
 										class="button album-get-button"
 										v-on:click="getAlbumData('genres')"
 										v-on:click="getAlbumData('genres')"
 									>
 									>
-										<i class="material-icons album-get-icon"
-											>album</i
-										>
+										<i class="material-icons">album</i>
 									</button>
 									</button>
 									<button
 									<button
 										class="button is-info add-button"
 										class="button is-info add-button"
@@ -532,7 +532,7 @@
 
 
 <script>
 <script>
 import { mapState, mapActions } from "vuex";
 import { mapState, mapActions } from "vuex";
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 
 
 import io from "../../io";
 import io from "../../io";
 import validation from "../../validation";
 import validation from "../../validation";
@@ -618,59 +618,61 @@ export default {
 			modals: state => state.modals.admin
 			modals: state => state.modals.admin
 		})
 		})
 	},
 	},
+	watch: {
+		/* eslint-disable */
+		"editing.song.duration": function() {
+			this.drawCanvas();
+		},
+		"editing.song.skipDuration": function() {
+			this.drawCanvas();
+		}
+		/* eslint-enable */
+	},
 	methods: {
 	methods: {
 		save(songToCopy, close) {
 		save(songToCopy, close) {
 			const song = JSON.parse(JSON.stringify(songToCopy));
 			const song = JSON.parse(JSON.stringify(songToCopy));
 
 
 			if (!song.title)
 			if (!song.title)
-				return Toast.methods.addToast(
-					"Please fill in all fields",
-					8000
-				);
+				return new Toast({
+					content: "Please fill in all fields",
+					timeout: 8000
+				});
 			if (!song.thumbnail)
 			if (!song.thumbnail)
-				return Toast.methods.addToast(
-					"Please fill in all fields",
-					8000
-				);
+				return new Toast({
+					content: "Please fill in all fields",
+					timeout: 8000
+				});
 
 
 			// Duration
 			// Duration
 			if (
 			if (
 				Number(song.skipDuration) + Number(song.duration) >
 				Number(song.skipDuration) + Number(song.duration) >
 				this.youtubeVideoDuration
 				this.youtubeVideoDuration
 			) {
 			) {
-				return Toast.methods.addToast(
-					"Duration can't be higher than the length of the video",
-					8000
-				);
+				return new Toast({
+					content:
+						"Duration can't be higher than the length of the video",
+					timeout: 8000
+				});
 			}
 			}
 
 
 			// Title
 			// Title
 			if (!validation.isLength(song.title, 1, 100))
 			if (!validation.isLength(song.title, 1, 100))
-				return Toast.methods.addToast(
-					"Title must have between 1 and 100 characters.",
-					8000
-				);
-			/* if (!validation.regex.ascii.test(song.title))
-				return Toast.methods.addToast(
-					"Invalid title format. Only ascii characters are allowed.",
-					8000
-				); */
+				return new Toast({
+					content: "Title must have between 1 and 100 characters.",
+					timeout: 8000
+				});
 
 
 			// Artists
 			// Artists
 			if (song.artists.length < 1 || song.artists.length > 10)
 			if (song.artists.length < 1 || song.artists.length > 10)
-				return Toast.methods.addToast(
-					"Invalid artists. You must have at least 1 artist and a maximum of 10 artists.",
-					8000
-				);
+				return new Toast({
+					content:
+						"Invalid artists. You must have at least 1 artist and a maximum of 10 artists.",
+					timeout: 8000
+				});
 			let error;
 			let error;
 			song.artists.forEach(artist => {
 			song.artists.forEach(artist => {
-				if (!validation.isLength(artist, 1, 32)) {
-					error = "Artist must have between 1 and 32 characters.";
-					return error;
-				}
-				if (!validation.regex.ascii.test(artist)) {
-					error =
-						"Invalid artist format. Only ascii characters are allowed.";
+				if (!validation.isLength(artist, 1, 64)) {
+					error = "Artist must have between 1 and 64 characters.";
 					return error;
 					return error;
 				}
 				}
 				if (artist === "NONE") {
 				if (artist === "NONE") {
@@ -681,16 +683,16 @@ export default {
 
 
 				return false;
 				return false;
 			});
 			});
-			if (error) return Toast.methods.addToast(error, 8000);
+			if (error) return new Toast({ content: error, timeout: 8000 });
 
 
 			// Genres
 			// Genres
-			/* error = undefined;
+			error = undefined;
 			song.genres.forEach(genre => {
 			song.genres.forEach(genre => {
-				if (!validation.isLength(genre, 1, 16)) {
-					error = "Genre must have between 1 and 16 characters.";
+				if (!validation.isLength(genre, 1, 32)) {
+					error = "Genre must have between 1 and 32 characters.";
 					return error;
 					return error;
 				}
 				}
-				if (!validation.regex.azAZ09_.test(genre)) {
+				if (!validation.regex.ascii.test(genre)) {
 					error =
 					error =
 						"Invalid genre format. Only ascii characters are allowed.";
 						"Invalid genre format. Only ascii characters are allowed.";
 					return error;
 					return error;
@@ -698,19 +700,22 @@ export default {
 
 
 				return false;
 				return false;
 			});
 			});
-			if (error) return Toast.methods.addToast(error, 8000); */
+			if (song.genres.length < 1 || song.genres.length > 16)
+				error = "You must have between 1 and 16 genres.";
+			if (error) return new Toast({ content: error, timeout: 8000 });
 
 
 			// Thumbnail
 			// Thumbnail
-			if (!validation.isLength(song.thumbnail, 8, 256))
-				return Toast.methods.addToast(
-					"Thumbnail must have between 8 and 256 characters.",
-					8000
-				);
+			if (!validation.isLength(song.thumbnail, 1, 256))
+				return new Toast({
+					content:
+						"Thumbnail must have between 8 and 256 characters.",
+					timeout: 8000
+				});
 			if (this.useHTTPS && song.thumbnail.indexOf("https://") !== 0) {
 			if (this.useHTTPS && song.thumbnail.indexOf("https://") !== 0) {
-				return Toast.methods.addToast(
-					'Thumbnail must start with "https://".',
-					8000
-				);
+				return new Toast({
+					content: 'Thumbnail must start with "https://".',
+					timeout: 8000
+				});
 			}
 			}
 
 
 			if (
 			if (
@@ -718,10 +723,10 @@ export default {
 				(song.thumbnail.indexOf("http://") !== 0 &&
 				(song.thumbnail.indexOf("http://") !== 0 &&
 					song.thumbnail.indexOf("https://") !== 0)
 					song.thumbnail.indexOf("https://") !== 0)
 			) {
 			) {
-				return Toast.methods.addToast(
-					'Thumbnail must start with "http://".',
-					8000
-				);
+				return new Toast({
+					content: 'Thumbnail must start with "http://".',
+					timeout: 8000
+				});
 			}
 			}
 
 
 			return this.socket.emit(
 			return this.socket.emit(
@@ -729,7 +734,7 @@ export default {
 				song._id,
 				song._id,
 				song,
 				song,
 				res => {
 				res => {
-					Toast.methods.addToast(res.message, 4000);
+					new Toast({ content: res.message, timeout: 4000 });
 					if (res.status === "success") {
 					if (res.status === "success") {
 						this.songs.forEach(originalSong => {
 						this.songs.forEach(originalSong => {
 							const updatedSong = song;
 							const updatedSong = song;
@@ -761,7 +766,7 @@ export default {
 					.then(data => {
 					.then(data => {
 						apiResult.album.artists = [];
 						apiResult.album.artists = [];
 						apiResult.album.artistIds = [];
 						apiResult.album.artistIds = [];
-						const artistRegex = new RegExp(" \\([0-9]\\)$");
+						const artistRegex = new RegExp(" \\([0-9]+\\)$");
 
 
 						apiResult.dataQuality = data.data_quality;
 						apiResult.dataQuality = data.data_quality;
 						data.artists.forEach(artist => {
 						data.artists.forEach(artist => {
@@ -781,6 +786,10 @@ export default {
 					});
 					});
 			}
 			}
 		},
 		},
+		fillDuration() {
+			this.editing.song.duration =
+				this.youtubeVideoDuration - this.editing.song.skipDuration;
+		},
 		getAlbumData(type) {
 		getAlbumData(type) {
 			if (!this.editing.song.discogs) return;
 			if (!this.editing.song.discogs) return;
 			if (type === "title")
 			if (type === "title")
@@ -796,12 +805,16 @@ export default {
 			if (type === "genres")
 			if (type === "genres")
 				this.updateSongField({
 				this.updateSongField({
 					field: "genres",
 					field: "genres",
-					value: this.editing.song.discogs.album.genres
+					value: JSON.parse(
+						JSON.stringify(this.editing.song.discogs.album.genres)
+					)
 				});
 				});
 			if (type === "artists")
 			if (type === "artists")
 				this.updateSongField({
 				this.updateSongField({
 					field: "artists",
 					field: "artists",
-					value: this.editing.song.discogs.album.artists
+					value: JSON.parse(
+						JSON.stringify(this.editing.song.discogs.album.artists)
+					)
 				});
 				});
 		},
 		},
 		searchDiscogsForPage(page) {
 		searchDiscogsForPage(page) {
@@ -810,15 +823,15 @@ export default {
 			this.socket.emit("apis.searchDiscogs", query, page, res => {
 			this.socket.emit("apis.searchDiscogs", query, page, res => {
 				if (res.status === "success") {
 				if (res.status === "success") {
 					if (page === 1)
 					if (page === 1)
-						Toast.methods.addToast(
-							`Successfully searched. Got ${res.results.length} results.`,
-							4000
-						);
+						new Toast({
+							content: `Successfully searched. Got ${res.results.length} results.`,
+							timeout: 4000
+						});
 					else
 					else
-						Toast.methods.addToast(
-							`Successfully got ${res.results.length} more results.`,
-							4000
-						);
+						new Toast({
+							content: `Successfully got ${res.results.length} more results.`,
+							timeout: 4000
+						});
 
 
 					if (page === 1) {
 					if (page === 1) {
 						this.discogs.apiResults = [];
 						this.discogs.apiResults = [];
@@ -850,7 +863,7 @@ export default {
 
 
 					this.discogs.page = page;
 					this.discogs.page = page;
 					this.discogs.disableLoadMore = false;
 					this.discogs.disableLoadMore = false;
-				} else Toast.methods.addToast(res.message, 8000);
+				} else new Toast({ content: res.message, timeout: 8000 });
 			});
 			});
 		},
 		},
 		loadNextDiscogsPage() {
 		loadNextDiscogsPage() {
@@ -937,6 +950,7 @@ export default {
 					this.pauseVideo(false);
 					this.pauseVideo(false);
 					break;
 					break;
 				case "skipToLast10Secs":
 				case "skipToLast10Secs":
+					if (this.video.paused) this.pauseVideo(false);
 					this.video.player.seekTo(
 					this.video.player.seekTo(
 						this.editing.song.duration -
 						this.editing.song.duration -
 							10 +
 							10 +
@@ -958,28 +972,37 @@ export default {
 					.value.toLowerCase()
 					.value.toLowerCase()
 					.trim();
 					.trim();
 				if (this.editing.song.genres.indexOf(genre) !== -1)
 				if (this.editing.song.genres.indexOf(genre) !== -1)
-					return Toast.methods.addToast("Genre already exists", 3000);
+					return new Toast({
+						content: "Genre already exists",
+						timeout: 3000
+					});
 				if (genre) {
 				if (genre) {
 					this.editing.song.genres.push(genre);
 					this.editing.song.genres.push(genre);
 					document.getElementById("new-genre").value = "";
 					document.getElementById("new-genre").value = "";
 					return false;
 					return false;
 				}
 				}
 
 
-				return Toast.methods.addToast("Genre cannot be empty", 3000);
+				return new Toast({
+					content: "Genre cannot be empty",
+					timeout: 3000
+				});
 			}
 			}
 			if (type === "artists") {
 			if (type === "artists") {
 				const artist = document.getElementById("new-artist").value;
 				const artist = document.getElementById("new-artist").value;
 				if (this.editing.song.artists.indexOf(artist) !== -1)
 				if (this.editing.song.artists.indexOf(artist) !== -1)
-					return Toast.methods.addToast(
-						"Artist already exists",
-						3000
-					);
+					return new Toast({
+						content: "Artist already exists",
+						timeout: 3000
+					});
 				if (document.getElementById("new-artist").value !== "") {
 				if (document.getElementById("new-artist").value !== "") {
 					this.editing.song.artists.push(artist);
 					this.editing.song.artists.push(artist);
 					document.getElementById("new-artist").value = "";
 					document.getElementById("new-artist").value = "";
 					return false;
 					return false;
 				}
 				}
-				return Toast.methods.addToast("Artist cannot be empty", 3000);
+				return new Toast({
+					content: "Artist cannot be empty",
+					timeout: 3000
+				});
 			}
 			}
 
 
 			return false;
 			return false;
@@ -999,7 +1022,7 @@ export default {
 			const duration = Number(this.editing.song.duration);
 			const duration = Number(this.editing.song.duration);
 			const afterDuration = videoDuration - (skipDuration + duration);
 			const afterDuration = videoDuration - (skipDuration + duration);
 
 
-			const width = 560;
+			const width = 530;
 
 
 			const currentTime = this.video.player.getCurrentTime();
 			const currentTime = this.video.player.getCurrentTime();
 
 
@@ -1135,8 +1158,8 @@ export default {
 
 
 		this.discogsQuery = this.editing.song.title;
 		this.discogsQuery = this.editing.song.title;
 
 
-		lofig.get("cookie.secure", res => {
-			this.useHTTPS = res;
+		lofig.get("cookie.secure").then(useHTTPS => {
+			this.useHTTPS = useHTTPS;
 		});
 		});
 
 
 		io.getSocket(socket => {
 		io.getSocket(socket => {
@@ -1145,6 +1168,7 @@ export default {
 
 
 		this.interval = setInterval(() => {
 		this.interval = setInterval(() => {
 			if (
 			if (
+				this.editing.song.duration !== -1 &&
 				this.video.paused === false &&
 				this.video.paused === false &&
 				this.playerReady &&
 				this.playerReady &&
 				this.video.player.getCurrentTime() -
 				this.video.player.getCurrentTime() -
@@ -1153,6 +1177,7 @@ export default {
 			) {
 			) {
 				this.video.paused = false;
 				this.video.paused = false;
 				this.video.player.stopVideo();
 				this.video.player.stopVideo();
+				this.drawCanvas();
 			}
 			}
 			if (this.playerReady) {
 			if (this.playerReady) {
 				this.youtubeVideoCurrentTime = this.video.player
 				this.youtubeVideoCurrentTime = this.video.player
@@ -1190,6 +1215,8 @@ export default {
 					this.drawCanvas();
 					this.drawCanvas();
 				},
 				},
 				onStateChange: event => {
 				onStateChange: event => {
+					this.drawCanvas();
+
 					if (event.data === 1) {
 					if (event.data === 1) {
 						if (!this.video.autoPlayed) {
 						if (!this.video.autoPlayed) {
 							this.video.autoPlayed = true;
 							this.video.autoPlayed = true;
@@ -1200,22 +1227,28 @@ export default {
 						let youtubeDuration = this.video.player.getDuration();
 						let youtubeDuration = this.video.player.getDuration();
 						this.youtubeVideoDuration = youtubeDuration;
 						this.youtubeVideoDuration = youtubeDuration;
 						this.youtubeVideoNote = "";
 						this.youtubeVideoNote = "";
+
+						if (this.editing.song.duration === -1)
+							this.editing.song.duration = youtubeDuration;
+
 						youtubeDuration -= this.editing.song.skipDuration;
 						youtubeDuration -= this.editing.song.skipDuration;
 						if (this.editing.song.duration > youtubeDuration + 1) {
 						if (this.editing.song.duration > youtubeDuration + 1) {
 							this.video.player.stopVideo();
 							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
-							);
+							return new Toast({
+								content:
+									"Video can't play. Specified duration is bigger than the YouTube song duration.",
+								timeout: 4000
+							});
 						}
 						}
 						if (this.editing.song.duration <= 0) {
 						if (this.editing.song.duration <= 0) {
 							this.video.player.stopVideo();
 							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
-							);
+							return new Toast({
+								content:
+									"Video can't play. Specified duration has to be more than 0 seconds.",
+								timeout: 4000
+							});
 						}
 						}
 
 
 						if (
 						if (
@@ -1404,6 +1437,14 @@ export default {
 			border-width: 0;
 			border-width: 0;
 		}
 		}
 
 
+		.duration-fill-button {
+			background-color: $red;
+			color: $white;
+			width: 32px;
+			text-align: center;
+			border-width: 0;
+		}
+
 		.add-button {
 		.add-button {
 			background-color: $musareBlue !important;
 			background-color: $musareBlue !important;
 			width: 32px;
 			width: 32px;

+ 869 - 174
frontend/components/Modals/EditStation.vue

@@ -1,79 +1,280 @@
 <template>
 <template>
-	<modal title="Edit Station">
+	<modal title="Edit Station" class="edit-station-modal">
 		<template v-slot:body>
 		<template v-slot:body>
-			<label class="label">Name</label>
-			<p class="control">
-				<input
-					v-model="editing.name"
-					class="input"
-					type="text"
-					placeholder="Station Name"
-				/>
-			</p>
-			<label class="label">Display name</label>
-			<p class="control">
-				<input
-					v-model="editing.displayName"
-					class="input"
-					type="text"
-					placeholder="Station Display Name"
-				/>
-			</p>
-			<label class="label">Description</label>
-			<p class="control">
-				<input
-					v-model="editing.description"
-					class="input"
-					type="text"
-					placeholder="Station Description"
-				/>
-			</p>
-			<label class="label">Privacy</label>
-			<p class="control">
-				<span class="select">
-					<select v-model="editing.privacy">
-						<option value="public">Public</option>
-						<option value="unlisted">Unlisted</option>
-						<option value="private">Private</option>
-					</select>
-				</span>
-			</p>
-			<br />
-			<p class="control">
-				<label class="checkbox party-mode-inner">
-					<input v-model="editing.partyMode" type="checkbox" />
-					&nbsp;Party mode
-				</label>
-			</p>
-			<small
-				>With party mode enabled, people can add songs to a queue that
-				plays. With party mode disabled you can play a private playlist
-				on loop.</small
-			>
-			<br />
-			<div v-if="station.partyMode">
-				<br />
-				<br />
-				<label class="label">Queue lock</label>
-				<small v-if="station.partyMode"
-					>With the queue locked, only owners (you) can add songs to
-					the queue.</small
-				>
-				<br />
-				<button
-					v-if="!station.locked"
-					class="button is-danger"
-					@click="$parent.toggleLock()"
-				>
-					Lock the queue
-				</button>
-				<button
-					v-if="station.locked"
-					class="button is-success"
-					@click="$parent.toggleLock()"
+			<div class="section left-section">
+				<div class="col col-2">
+					<div>
+						<label class="label">Name</label>
+						<p class="control">
+							<input
+								class="input"
+								type="text"
+								v-model="editing.name"
+							/>
+						</p>
+					</div>
+					<div>
+						<label class="label">Display name</label>
+						<p class="control">
+							<input
+								class="input"
+								type="text"
+								v-model="editing.displayName"
+							/>
+						</p>
+					</div>
+				</div>
+				<div class="col col-1">
+					<div>
+						<label class="label">Description</label>
+						<p class="control">
+							<input
+								class="input"
+								type="text"
+								v-model="editing.description"
+							/>
+						</p>
+					</div>
+				</div>
+				<div class="col col-2" v-if="editing.genres">
+					<div>
+						<label class="label">Genre(s)</label>
+						<p class="control has-addons">
+							<input
+								class="input"
+								type="text"
+								id="new-genre"
+								v-model="genreInputValue"
+								v-on:blur="blurGenreInput()"
+								v-on:focus="focusGenreInput()"
+								v-on:keydown="keydownGenreInput()"
+								v-on:keyup.enter="addTag('genres')"
+							/>
+							<button
+								class="button is-info add-button blue"
+								v-on:click="addTag('genres')"
+							>
+								<i class="material-icons">add</i>
+							</button>
+						</p>
+						<div
+							class="autosuggest-container"
+							v-if="
+								(genreInputFocussed ||
+									genreAutosuggestContainerFocussed) &&
+									genreAutosuggestItems.length > 0
+							"
+							@mouseover="focusGenreContainer()"
+							@mouseleave="blurGenreContainer()"
+						>
+							<span
+								class="autosuggest-item"
+								tabindex="0"
+								v-on:click="selectGenreAutosuggest(item)"
+								v-for="(item, index) in genreAutosuggestItems"
+								:key="index"
+								>{{ item }}</span
+							>
+						</div>
+						<div class="list-container">
+							<div
+								class="list-item"
+								v-for="(genre, index) in editing.genres"
+								:key="index"
+							>
+								<div
+									class="list-item-circle blue"
+									v-on:click="removeTag('genres', index)"
+								>
+									<i class="material-icons">close</i>
+								</div>
+								<p>{{ genre }}</p>
+							</div>
+						</div>
+					</div>
+					<div>
+						<label class="label">Blacklist genre(s)</label>
+						<p class="control has-addons">
+							<input
+								class="input"
+								type="text"
+								v-model="blacklistGenreInputValue"
+								v-on:blur="blurBlacklistGenreInput()"
+								v-on:focus="focusBlacklistGenreInput()"
+								v-on:keydown="keydownBlacklistGenreInput()"
+								v-on:keyup.enter="addTag('blacklist-genres')"
+							/>
+							<button
+								class="button is-info add-button red"
+								v-on:click="addTag('blacklist-genres')"
+							>
+								<i class="material-icons">add</i>
+							</button>
+						</p>
+						<div
+							class="autosuggest-container"
+							v-if="
+								(blacklistGenreInputFocussed ||
+									blacklistGenreAutosuggestContainerFocussed) &&
+									blacklistGenreAutosuggestItems.length > 0
+							"
+							@mouseover="focusBlacklistGenreContainer()"
+							@mouseleave="blurBlacklistGenreContainer()"
+						>
+							<span
+								class="autosuggest-item"
+								tabindex="0"
+								v-on:click="
+									selectBlacklistGenreAutosuggest(item)
+								"
+								v-for="(item,
+								index) in blacklistGenreAutosuggestItems"
+								:key="index"
+								>{{ item }}</span
+							>
+						</div>
+						<div class="list-container">
+							<div
+								class="list-item"
+								v-for="(genre,
+								index) in editing.blacklistedGenres"
+								:key="index"
+							>
+								<div
+									class="list-item-circle red"
+									v-on:click="
+										removeTag('blacklist-genres', index)
+									"
+								>
+									<i class="material-icons">close</i>
+								</div>
+								<p>{{ genre }}</p>
+							</div>
+						</div>
+					</div>
+				</div>
+			</div>
+			<div class="section right-section">
+				<div>
+					<label class="label">Privacy</label>
+					<div
+						@mouseenter="privacyDropdownActive = true"
+						@mouseleave="privacyDropdownActive = false"
+						class="button-wrapper"
+					>
+						<button
+							v-bind:class="{
+								green: true,
+								current: editing.privacy === 'public'
+							}"
+							v-if="
+								privacyDropdownActive ||
+									editing.privacy === 'public'
+							"
+							@click="updatePrivacyLocal('public')"
+						>
+							<i class="material-icons">public</i>
+							Public
+						</button>
+						<button
+							v-bind:class="{
+								orange: true,
+								current: editing.privacy === 'unlisted'
+							}"
+							v-if="
+								privacyDropdownActive ||
+									editing.privacy === 'unlisted'
+							"
+							@click="updatePrivacyLocal('unlisted')"
+						>
+							<i class="material-icons">link</i>
+							Unlisted
+						</button>
+						<button
+							v-bind:class="{
+								red: true,
+								current: editing.privacy === 'private'
+							}"
+							v-if="
+								privacyDropdownActive ||
+									editing.privacy === 'private'
+							"
+							@click="updatePrivacyLocal('private')"
+						>
+							<i class="material-icons">lock</i>
+							Private
+						</button>
+					</div>
+				</div>
+				<div v-if="editing.type === 'community'">
+					<label class="label">Mode</label>
+					<div
+						@mouseenter="modeDropdownActive = true"
+						@mouseleave="modeDropdownActive = false"
+						class="button-wrapper"
+					>
+						<button
+							v-bind:class="{
+								blue: true,
+								current: editing.partyMode === false
+							}"
+							v-if="modeDropdownActive || !editing.partyMode"
+							@click="updatePartyModeLocal(false)"
+						>
+							<i class="material-icons">playlist_play</i>
+							Playlist
+						</button>
+						<button
+							v-bind:class="{
+								yellow: true,
+								current: editing.partyMode === true
+							}"
+							v-if="
+								modeDropdownActive || editing.partyMode === true
+							"
+							@click="updatePartyModeLocal(true)"
+						>
+							<i class="material-icons">emoji_people</i>
+							Party
+						</button>
+					</div>
+				</div>
+				<div
+					v-if="
+						editing.type === 'community' &&
+							editing.partyMode === true
+					"
 				>
 				>
-					Unlock the queue
-				</button>
+					<label class="label">Queue lock</label>
+					<div
+						@mouseenter="queueLockDropdownActive = true"
+						@mouseleave="queueLockDropdownActive = false"
+						class="button-wrapper"
+					>
+						<button
+							v-bind:class="{
+								green: true,
+								current: editing.locked
+							}"
+							v-if="queueLockDropdownActive || editing.locked"
+							@click="updateQueueLockLocal(true)"
+						>
+							<i class="material-icons">lock</i>
+							On
+						</button>
+						<button
+							v-bind:class="{
+								red: true,
+								current: !editing.locked
+							}"
+							v-if="queueLockDropdownActive || !editing.locked"
+							@click="updateQueueLockLocal(false)"
+						>
+							<i class="material-icons">lock_open</i>
+							Off
+						</button>
+					</div>
+				</div>
 			</div>
 			</div>
 		</template>
 		</template>
 		<template v-slot:footer>
 		<template v-slot:footer>
@@ -92,17 +293,23 @@
 </template>
 </template>
 
 
 <script>
 <script>
-import { mapState } from "vuex";
+import { mapState, mapActions } from "vuex";
 
 
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 import Modal from "./Modal.vue";
 import Modal from "./Modal.vue";
 import io from "../../io";
 import io from "../../io";
 import validation from "../../validation";
 import validation from "../../validation";
 
 
 export default {
 export default {
-	computed: mapState("station", {
-		station: state => state.station,
-		editing: state => state.editing
+	computed: mapState({
+		editing(state) {
+			return this.$props.store.split("/").reduce((a, v) => a[v], state)
+				.editing;
+		},
+		station(state) {
+			return this.$props.store.split("/").reduce((a, v) => a[v], state)
+				.station;
+		}
 	}),
 	}),
 	mounted() {
 	mounted() {
 		io.getSocket(socket => {
 		io.getSocket(socket => {
@@ -110,6 +317,56 @@ export default {
 			return socket;
 			return socket;
 		});
 		});
 	},
 	},
+	data() {
+		return {
+			genreInputValue: "",
+			genreInputFocussed: false,
+			genreAutosuggestContainerFocussed: false,
+			keydownGenreInputTimeout: 0,
+			genreAutosuggestItems: [],
+			blacklistGenreInputValue: "",
+			blacklistGenreInputFocussed: false,
+			blacklistGenreAutosuggestContainerFocussed: false,
+			blacklistKeydownGenreInputTimeout: 0,
+			blacklistGenreAutosuggestItems: [],
+			privacyDropdownActive: false,
+			modeDropdownActive: false,
+			queueLockDropdownActive: false,
+			genres: [
+				"Blues",
+				"Country",
+				"Disco",
+				"Funk",
+				"Hip-Hop",
+				"Jazz",
+				"Metal",
+				"Oldies",
+				"Other",
+				"Pop",
+				"Rap",
+				"Reggae",
+				"Rock",
+				"Techno",
+				"Trance",
+				"Classical",
+				"Instrumental",
+				"House",
+				"Electronic",
+				"Christian Rap",
+				"Lo-Fi",
+				"Musical",
+				"Rock 'n' Roll",
+				"Opera",
+				"Drum & Bass",
+				"Club-House",
+				"Indie",
+				"Heavy Metal",
+				"Christian rock",
+				"Dubstep"
+			]
+		};
+	},
+	props: ["store"],
 	methods: {
 	methods: {
 		update() {
 		update() {
 			if (this.station.name !== this.editing.name) this.updateName();
 			if (this.station.name !== this.editing.name) this.updateName();
@@ -119,21 +376,43 @@ export default {
 				this.updateDescription();
 				this.updateDescription();
 			if (this.station.privacy !== this.editing.privacy)
 			if (this.station.privacy !== this.editing.privacy)
 				this.updatePrivacy();
 				this.updatePrivacy();
-			if (this.station.partyMode !== this.editing.partyMode)
+			if (
+				this.station.type === "community" &&
+				this.station.partyMode !== this.editing.partyMode
+			)
 				this.updatePartyMode();
 				this.updatePartyMode();
+			if (
+				this.station.type === "community" &&
+				this.editing.partyMode &&
+				this.station.locked !== this.editing.locked
+			)
+				this.updateQueueLock();
+			if (this.$props.store !== "station") {
+				if (
+					this.station.genres.toString() !==
+					this.editing.genres.toString()
+				)
+					this.updateGenres();
+				if (
+					this.station.blacklistedGenres.toString() !==
+					this.editing.blacklistedGenres.toString()
+				)
+					this.updateBlacklistedGenres();
+			}
 		},
 		},
 		updateName() {
 		updateName() {
 			const { name } = this.editing;
 			const { name } = this.editing;
 			if (!validation.isLength(name, 2, 16))
 			if (!validation.isLength(name, 2, 16))
-				return Toast.methods.addToast(
-					"Name must have between 2 and 16 characters.",
-					8000
-				);
+				return new Toast({
+					content: "Name must have between 2 and 16 characters.",
+					timeout: 8000
+				});
 			if (!validation.regex.az09_.test(name))
 			if (!validation.regex.az09_.test(name))
-				return Toast.methods.addToast(
-					"Invalid name format. Allowed characters: a-z, 0-9 and _.",
-					8000
-				);
+				return new Toast({
+					content:
+						"Invalid name format. Allowed characters: a-z, 0-9 and _.",
+					timeout: 8000
+				});
 
 
 			return this.socket.emit(
 			return this.socket.emit(
 				"stations.updateName",
 				"stations.updateName",
@@ -141,37 +420,37 @@ export default {
 				name,
 				name,
 				res => {
 				res => {
 					if (res.status === "success") {
 					if (res.status === "success") {
-						if (this.station) {
-							this.station.name = name;
-							return name;
-						}
-
-						this.$parent.stations.forEach((station, index) => {
-							if (station._id === this.editing._id) {
-								this.$parent.stations[index].name = name;
-								return name;
-							}
+						if (this.station) this.station.name = name;
+						else {
+							this.$parent.stations.forEach((station, index) => {
+								if (station._id === this.editing._id) {
+									this.$parent.stations[index].name = name;
+									return name;
+								}
 
 
-							return false;
-						});
+								return false;
+							});
+						}
 					}
 					}
 
 
-					return Toast.methods.addToast(res.message, 8000);
+					new Toast({ content: res.message, timeout: 8000 });
 				}
 				}
 			);
 			);
 		},
 		},
 		updateDisplayName() {
 		updateDisplayName() {
 			const { displayName } = this.editing;
 			const { displayName } = this.editing;
 			if (!validation.isLength(displayName, 2, 32))
 			if (!validation.isLength(displayName, 2, 32))
-				return Toast.methods.addToast(
-					"Display name must have between 2 and 32 characters.",
-					8000
-				);
-			if (!validation.regex.azAZ09_.test(displayName))
-				return Toast.methods.addToast(
-					"Invalid display name format. Allowed characters: a-z, A-Z, 0-9 and _.",
-					8000
-				);
+				return new Toast({
+					content:
+						"Display name must have between 2 and 32 characters.",
+					timeout: 8000
+				});
+			if (!validation.regex.ascii.test(displayName))
+				return new Toast({
+					content:
+						"Invalid display name format. Only ASCII characters are allowed.",
+					timeout: 8000
+				});
 
 
 			return this.socket.emit(
 			return this.socket.emit(
 				"stations.updateDisplayName",
 				"stations.updateDisplayName",
@@ -183,36 +462,40 @@ export default {
 							this.station.displayName = displayName;
 							this.station.displayName = displayName;
 						else {
 						else {
 							this.$parent.stations.forEach((station, index) => {
 							this.$parent.stations.forEach((station, index) => {
-								if (station._id === this.editing._id)
+								if (station._id === this.editing._id) {
 									this.$parent.stations[
 									this.$parent.stations[
 										index
 										index
 									].displayName = displayName;
 									].displayName = displayName;
-								return displayName;
+									return displayName;
+								}
+
+								return false;
 							});
 							});
 						}
 						}
 					}
 					}
-					Toast.methods.addToast(res.message, 8000);
+
+					new Toast({ content: res.message, timeout: 8000 });
 				}
 				}
 			);
 			);
 		},
 		},
 		updateDescription() {
 		updateDescription() {
 			const { description } = this.editing;
 			const { description } = this.editing;
 			if (!validation.isLength(description, 2, 200))
 			if (!validation.isLength(description, 2, 200))
-				return Toast.methods.addToast(
-					"Description must have between 2 and 200 characters.",
-					8000
-				);
+				return new Toast({
+					content:
+						"Description must have between 2 and 200 characters.",
+					timeout: 8000
+				});
 
 
 			let characters = description.split("");
 			let characters = description.split("");
 			characters = characters.filter(character => {
 			characters = characters.filter(character => {
 				return character.charCodeAt(0) === 21328;
 				return character.charCodeAt(0) === 21328;
 			});
 			});
-
 			if (characters.length !== 0)
 			if (characters.length !== 0)
-				return Toast.methods.addToast(
-					"Invalid description format. Swastika's are not allowed.",
-					8000
-				);
+				return new Toast({
+					content: "Invalid description format.",
+					timeout: 8000
+				});
 
 
 			return this.socket.emit(
 			return this.socket.emit(
 				"stations.updateDescription",
 				"stations.updateDescription",
@@ -220,126 +503,538 @@ export default {
 				description,
 				description,
 				res => {
 				res => {
 					if (res.status === "success") {
 					if (res.status === "success") {
-						if (this.station) {
+						if (this.station)
 							this.station.description = description;
 							this.station.description = description;
-							return description;
-						}
+						else {
+							this.$parent.stations.forEach((station, index) => {
+								if (station._id === this.editing._id) {
+									this.$parent.stations[
+										index
+									].description = description;
+									return description;
+								}
 
 
-						this.$parent.stations.forEach((station, index) => {
-							if (station._id === this.editing._id) {
-								this.$parent.stations[
-									index
-								].description = description;
-								return description;
-							}
+								return false;
+							});
+						}
 
 
-							return false;
+						return new Toast({
+							content: res.message,
+							timeout: 4000
 						});
 						});
-
-						return Toast.methods.addToast(res.message, 4000);
 					}
 					}
 
 
-					return Toast.methods.addToast(res.message, 8000);
+					return new Toast({ content: res.message, timeout: 8000 });
 				}
 				}
 			);
 			);
 		},
 		},
+		updatePrivacyLocal(privacy) {
+			if (this.editing.privacy === privacy) return;
+			this.editing.privacy = privacy;
+			this.privacyDropdownActive = false;
+		},
 		updatePrivacy() {
 		updatePrivacy() {
-			return this.socket.emit(
+			this.socket.emit(
 				"stations.updatePrivacy",
 				"stations.updatePrivacy",
 				this.editing._id,
 				this.editing._id,
 				this.editing.privacy,
 				this.editing.privacy,
 				res => {
 				res => {
 					if (res.status === "success") {
 					if (res.status === "success") {
-						if (this.station) {
+						if (this.station)
 							this.station.privacy = this.editing.privacy;
 							this.station.privacy = this.editing.privacy;
-							return 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;
+								}
+
+								return false;
+							});
 						}
 						}
+						return new Toast({
+							content: res.message,
+							timeout: 4000
+						});
+					}
 
 
+					return new Toast({ content: res.message, timeout: 8000 });
+				}
+			);
+		},
+		updateGenres() {
+			this.socket.emit(
+				"stations.updateGenres",
+				this.editing._id,
+				this.editing.genres,
+				res => {
+					if (res.status === "success") {
+						const genres = JSON.parse(
+							JSON.stringify(this.editing.genres)
+						);
+						if (this.station) this.station.genres = genres;
 						this.$parent.stations.forEach((station, index) => {
 						this.$parent.stations.forEach((station, index) => {
 							if (station._id === this.editing._id) {
 							if (station._id === this.editing._id) {
-								this.$parent.stations[
-									index
-								].privacy = this.editing.privacy;
-								return this.editing.privacy;
+								this.$parent.stations[index].genres = genres;
+								return genres;
 							}
 							}
 
 
 							return false;
 							return false;
 						});
 						});
 
 
-						return Toast.methods.addToast(res.message, 4000);
+						return new Toast({
+							content: res.message,
+							timeout: 4000
+						});
 					}
 					}
 
 
-					return Toast.methods.addToast(res.message, 8000);
+					return new Toast({ content: res.message, timeout: 8000 });
 				}
 				}
 			);
 			);
 		},
 		},
-		updatePartyMode() {
-			return this.socket.emit(
-				"stations.updatePartyMode",
+		updateBlacklistedGenres() {
+			this.socket.emit(
+				"stations.updateBlacklistedGenres",
 				this.editing._id,
 				this.editing._id,
-				this.editing.partyMode,
+				this.editing.blacklistedGenres,
 				res => {
 				res => {
 					if (res.status === "success") {
 					if (res.status === "success") {
-						if (this.station) {
-							this.station.partyMode = this.editing.partyMode;
-							return this.editing.partyMode;
-						}
-
+						const blacklistedGenres = JSON.parse(
+							JSON.stringify(this.editing.blacklistedGenres)
+						);
+						if (this.station)
+							this.station.blacklistedGenres = blacklistedGenres;
 						this.$parent.stations.forEach((station, index) => {
 						this.$parent.stations.forEach((station, index) => {
 							if (station._id === this.editing._id) {
 							if (station._id === this.editing._id) {
 								this.$parent.stations[
 								this.$parent.stations[
 									index
 									index
-								].partyMode = this.editing.partyMode;
-								return this.editing.partyMode;
+								].blacklistedGenres = blacklistedGenres;
+								return blacklistedGenres;
 							}
 							}
 
 
 							return false;
 							return false;
 						});
 						});
+						return new Toast({
+							content: res.message,
+							timeout: 4000
+						});
+					}
 
 
-						return Toast.methods.addToast(res.message, 4000);
+					return new Toast({ content: res.message, timeout: 8000 });
+				}
+			);
+		},
+		updatePartyModeLocal(partyMode) {
+			if (this.editing.partyMode === partyMode) return;
+			this.editing.partyMode = partyMode;
+			this.modeDropdownActive = false;
+		},
+		updatePartyMode() {
+			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;
+						// 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;
+						// });
+
+						return new Toast({
+							content: res.message,
+							timeout: 4000
+						});
 					}
 					}
 
 
-					return Toast.methods.addToast(res.message, 8000);
+					return new Toast({ content: res.message, timeout: 8000 });
 				}
 				}
 			);
 			);
 		},
 		},
+		updateQueueLockLocal(locked) {
+			if (this.editing.locked === locked) return;
+			this.editing.locked = locked;
+			this.queueLockDropdownActive = false;
+		},
+		updateQueueLock() {
+			this.socket.emit("stations.toggleLock", this.editing._id, res => {
+				console.log(res);
+				if (res.status === "success") {
+					if (this.station) this.station.locked = res.data;
+					return new Toast({
+						content: `Toggled queue lock succesfully to ${res.data}`,
+						timeout: 4000
+					});
+				}
+				return new Toast({
+					content: "Failed to toggle queue lock.",
+					timeout: 8000
+				});
+			});
+		},
 		deleteStation() {
 		deleteStation() {
 			this.socket.emit("stations.remove", this.editing._id, res => {
 			this.socket.emit("stations.remove", this.editing._id, res => {
-				Toast.methods.addToast(res.message, 8000);
+				if (res.status === "success")
+					this.closeModal({
+						sector: "station",
+						modal: "editStation"
+					});
+				return new Toast({ content: res.message, timeout: 8000 });
 			});
 			});
-		}
+		},
+		blurGenreInput() {
+			this.genreInputFocussed = false;
+		},
+		focusGenreInput() {
+			this.genreInputFocussed = true;
+		},
+		keydownGenreInput() {
+			clearTimeout(this.keydownGenreInputTimeout);
+			this.keydownGenreInputTimeout = setTimeout(() => {
+				if (this.genreInputValue.length > 1) {
+					this.genreAutosuggestItems = this.genres.filter(genre => {
+						return genre
+							.toLowerCase()
+							.startsWith(this.genreInputValue.toLowerCase());
+					});
+				} else this.genreAutosuggestItems = [];
+			}, 1000);
+		},
+		focusGenreContainer() {
+			this.genreAutosuggestContainerFocussed = true;
+		},
+		blurGenreContainer() {
+			this.genreAutosuggestContainerFocussed = false;
+		},
+		selectGenreAutosuggest(value) {
+			this.genreInputValue = value;
+		},
+		blurBlacklistGenreInput() {
+			this.blacklistGenreInputFocussed = false;
+		},
+		focusBlacklistGenreInput() {
+			this.blacklistGenreInputFocussed = true;
+		},
+		keydownBlacklistGenreInput() {
+			clearTimeout(this.keydownBlacklistGenreInputTimeout);
+			this.keydownBlacklistGenreInputTimeout = setTimeout(() => {
+				if (this.blacklistGenreInputValue.length > 1) {
+					this.blacklistGenreAutosuggestItems = this.genres.filter(
+						genre => {
+							return genre
+								.toLowerCase()
+								.startsWith(
+									this.blacklistGenreInputValue.toLowerCase()
+								);
+						}
+					);
+				} else this.blacklistGenreAutosuggestItems = [];
+			}, 1000);
+		},
+		focusBlacklistGenreContainer() {
+			this.blacklistGenreAutosuggestContainerFocussed = true;
+		},
+		blurBlacklistGenreContainer() {
+			this.blacklistGenreAutosuggestContainerFocussed = false;
+		},
+		selectBlacklistGenreAutosuggest(value) {
+			this.blacklistGenreInputValue = value;
+		},
+		addTag(type) {
+			if (type === "genres") {
+				const genre = this.genreInputValue.toLowerCase().trim();
+				if (this.editing.genres.indexOf(genre) !== -1)
+					return new Toast({
+						content: "Genre already exists",
+						timeout: 3000
+					});
+				if (genre) {
+					this.editing.genres.push(genre);
+					this.genreInputValue = "";
+					return false;
+				}
+
+				return new Toast({
+					content: "Genre cannot be empty",
+					timeout: 3000
+				});
+			}
+			if (type === "blacklist-genres") {
+				const genre = this.blacklistGenreInputValue
+					.toLowerCase()
+					.trim();
+				if (this.editing.blacklistedGenres.indexOf(genre) !== -1)
+					return new Toast({
+						content: "Blacklist genre already exists",
+						timeout: 3000
+					});
+				if (genre) {
+					this.editing.blacklistedGenres.push(genre);
+					this.blacklistGenreInputValue = "";
+					return false;
+				}
+
+				return new Toast({
+					content: "Blacklist genre cannot be empty",
+					timeout: 3000
+				});
+			}
+
+			return false;
+		},
+		removeTag(type, index) {
+			if (type === "genres") this.editing.genres.splice(index, 1);
+			else if (type === "blacklist-genres")
+				this.editing.blacklistedGenres.splice(index, 1);
+		},
+		...mapActions("modals", ["closeModal"])
 	},
 	},
 	components: { Modal }
 	components: { Modal }
 };
 };
 </script>
 </script>
 
 
+<style lang="scss">
+.edit-station-modal {
+	.modal-card-title {
+		text-align: center;
+		margin-left: 24px;
+	}
+
+	.modal-card {
+		width: 800px;
+		height: 550px;
+
+		.modal-card-body {
+			padding: 16px;
+			display: flex;
+		}
+	}
+}
+</style>
+
 <style lang="scss" scoped>
 <style lang="scss" scoped>
 @import "styles/global.scss";
 @import "styles/global.scss";
 
 
-.controls {
-	display: flex;
+.section {
+	border: 1px solid #a3e0ff;
+	background-color: #f4f4f4;
+	border-radius: 5px;
+	padding: 16px;
+}
 
 
-	a {
-		display: flex;
-		align-items: center;
+.left-section {
+	width: 595px;
+	display: grid;
+	gap: 16px;
+	grid-template-rows: min-content min-content auto;
+
+	.control {
+		input {
+			width: 100%;
+		}
+
+		.add-button {
+			width: 32px;
+
+			&.blue {
+				background-color: $musareBlue !important;
+			}
+
+			&.red {
+				background-color: $red !important;
+			}
+
+			i {
+				font-size: 32px;
+			}
+		}
+	}
+
+	.col {
+		> div {
+			position: relative;
+		}
+	}
+
+	.list-item-circle {
+		width: 16px;
+		height: 16px;
+		border-radius: 8px;
+		cursor: pointer;
+		margin-right: 8px;
+		float: left;
+		-webkit-touch-callout: none;
+		-webkit-user-select: none;
+		-khtml-user-select: none;
+		-moz-user-select: none;
+		-ms-user-select: none;
+		user-select: none;
+
+		&.blue {
+			background-color: $musareBlue;
+
+			i {
+				color: $musareBlue;
+			}
+		}
+
+		&.red {
+			background-color: $red;
+
+			i {
+				color: $red;
+			}
+		}
+
+		i {
+			font-size: 14px;
+			margin-left: 1px;
+		}
+	}
+
+	.list-item-circle:hover,
+	.list-item-circle:focus {
+		i {
+			color: white;
+		}
+	}
+
+	.list-item > p {
+		line-height: 16px;
+		word-wrap: break-word;
+		width: calc(100% - 24px);
+		left: 24px;
+		float: left;
+		margin-bottom: 8px;
+	}
+
+	.list-item:last-child > p {
+		margin-bottom: 0;
+	}
+
+	.autosuggest-container {
+		position: absolute;
+		background: white;
+		width: calc(100% + 1px);
+		top: 57px;
+		z-index: 200;
+		overflow: auto;
+		max-height: 100%;
+		clear: both;
+
+		.autosuggest-item {
+			padding: 8px;
+			display: block;
+			border: 1px solid #dbdbdb;
+			margin-top: -1px;
+			line-height: 16px;
+			cursor: pointer;
+			-webkit-user-select: none;
+			-ms-user-select: none;
+			-moz-user-select: none;
+			user-select: none;
+		}
+
+		.autosuggest-item:hover,
+		.autosuggest-item:focus {
+			background-color: #eee;
+		}
+
+		.autosuggest-item:first-child {
+			border-top: none;
+		}
+
+		.autosuggest-item:last-child {
+			border-radius: 0 0 3px 3px;
+		}
 	}
 	}
 }
 }
 
 
-.table {
-	margin-bottom: 0;
+.right-section {
+	width: 157px;
+	margin-left: 16px;
+	display: grid;
+	gap: 16px;
+	grid-template-rows: min-content min-content min-content;
+
+	.button-wrapper {
+		display: flex;
+		flex-direction: column;
+	}
+
+	button {
+		width: 100%;
+		height: 36px;
+		border: 0;
+		border-radius: 10px;
+		font-size: 18px;
+		color: white;
+		box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.25);
+		display: block;
+		text-align: center;
+		justify-content: center;
+		display: inline-flex;
+		-ms-flex-align: center;
+		align-items: center;
+		-moz-user-select: none;
+		user-select: none;
+		cursor: pointer;
+		margin-bottom: 16px;
+		padding: 0;
+
+		&.current {
+			order: -1;
+		}
+
+		&.red {
+			background-color: $red;
+		}
+
+		&.green {
+			background-color: $green;
+		}
+
+		&.blue {
+			background-color: $musareBlue;
+		}
+
+		&.orange {
+			background-color: $light-orange;
+		}
+
+		&.yellow {
+			background-color: $yellow;
+		}
+
+		i {
+			font-size: 20px;
+			margin-right: 4px;
+		}
+	}
 }
 }
 
 
-h5 {
-	padding: 20px 0;
+.col {
+	display: grid;
+	grid-column-gap: 16px;
 }
 }
 
 
-.party-mode-inner,
-.party-mode-outer {
-	display: flex;
-	align-items: center;
+.col-1 {
+	grid-template-columns: auto;
 }
 }
 
 
-.select:after {
-	border-color: $primary-color;
+.col-2 {
+	grid-template-columns: auto auto;
 }
 }
 </style>
 </style>

+ 36 - 30
frontend/components/Modals/EditUser.vue

@@ -92,7 +92,7 @@
 <script>
 <script>
 import { mapState, mapActions } from "vuex";
 import { mapState, mapActions } from "vuex";
 
 
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 import io from "../../io";
 import io from "../../io";
 import Modal from "./Modal.vue";
 import Modal from "./Modal.vue";
 import validation from "../../validation";
 import validation from "../../validation";
@@ -118,44 +118,49 @@ export default {
 		updateUsername() {
 		updateUsername() {
 			const { username } = this.editing;
 			const { username } = this.editing;
 			if (!validation.isLength(username, 2, 32))
 			if (!validation.isLength(username, 2, 32))
-				return Toast.methods.addToast(
-					"Username must have between 2 and 32 characters.",
-					8000
-				);
-			if (!validation.regex.azAZ09_.test(username))
-				return Toast.methods.addToast(
-					"Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _.",
-					8000
-				);
+				return new Toast({
+					content: "Username must have between 2 and 32 characters.",
+					timeout: 8000
+				});
+			if (!validation.regex.custom("a-zA-Z0-9_-").test(username))
+				return new Toast({
+					content:
+						"Invalid username format. Allowed characters: a-z, A-Z, 0-9, _ and -.",
+					timeout: 8000
+				});
 
 
 			return this.socket.emit(
 			return this.socket.emit(
 				`users.updateUsername`,
 				`users.updateUsername`,
 				this.editing._id,
 				this.editing._id,
 				username,
 				username,
 				res => {
 				res => {
-					Toast.methods.addToast(res.message, 4000);
+					new Toast({ content: res.message, timeout: 4000 });
 				}
 				}
 			);
 			);
 		},
 		},
 		updateEmail() {
 		updateEmail() {
-			const { email } = this.editing;
+			const email = this.editing.email.address;
 			if (!validation.isLength(email, 3, 254))
 			if (!validation.isLength(email, 3, 254))
-				return Toast.methods.addToast(
-					"Email must have between 3 and 254 characters.",
-					8000
-				);
+				return new Toast({
+					content: "Email must have between 3 and 254 characters.",
+					timeout: 8000
+				});
 			if (
 			if (
 				email.indexOf("@") !== email.lastIndexOf("@") ||
 				email.indexOf("@") !== email.lastIndexOf("@") ||
-				!validation.regex.emailSimple.test(email)
+				!validation.regex.emailSimple.test(email) ||
+				!validation.regex.ascii.test(email)
 			)
 			)
-				return Toast.methods.addToast("Invalid email format.", 8000);
+				return new Toast({
+					content: "Invalid email format.",
+					timeout: 8000
+				});
 
 
 			return this.socket.emit(
 			return this.socket.emit(
 				`users.updateEmail`,
 				`users.updateEmail`,
 				this.editing._id,
 				this.editing._id,
 				email,
 				email,
 				res => {
 				res => {
-					Toast.methods.addToast(res.message, 4000);
+					new Toast({ content: res.message, timeout: 4000 });
 				}
 				}
 			);
 			);
 		},
 		},
@@ -165,7 +170,7 @@ export default {
 				this.editing._id,
 				this.editing._id,
 				this.editing.role,
 				this.editing.role,
 				res => {
 				res => {
-					Toast.methods.addToast(res.message, 4000);
+					new Toast({ content: res.message, timeout: 4000 });
 					if (
 					if (
 						res.status === "success" &&
 						res.status === "success" &&
 						this.editing.role === "default" &&
 						this.editing.role === "default" &&
@@ -178,15 +183,16 @@ export default {
 		banUser() {
 		banUser() {
 			const { reason } = this.ban;
 			const { reason } = this.ban;
 			if (!validation.isLength(reason, 1, 64))
 			if (!validation.isLength(reason, 1, 64))
-				return Toast.methods.addToast(
-					"Reason must have between 1 and 64 characters.",
-					8000
-				);
+				return new Toast({
+					content: "Reason must have between 1 and 64 characters.",
+					timeout: 8000
+				});
 			if (!validation.regex.ascii.test(reason))
 			if (!validation.regex.ascii.test(reason))
-				return Toast.methods.addToast(
-					"Invalid reason format. Only ascii characters are allowed.",
-					8000
-				);
+				return new Toast({
+					content:
+						"Invalid reason format. Only ascii characters are allowed.",
+					timeout: 8000
+				});
 
 
 			return this.socket.emit(
 			return this.socket.emit(
 				`users.banUserById`,
 				`users.banUserById`,
@@ -194,13 +200,13 @@ export default {
 				this.ban.reason,
 				this.ban.reason,
 				this.ban.expiresAt,
 				this.ban.expiresAt,
 				res => {
 				res => {
-					Toast.methods.addToast(res.message, 4000);
+					new Toast({ content: res.message, timeout: 4000 });
 				}
 				}
 			);
 			);
 		},
 		},
 		removeSessions() {
 		removeSessions() {
 			this.socket.emit(`users.removeSessions`, this.editing._id, res => {
 			this.socket.emit(`users.removeSessions`, this.editing._id, res => {
-				Toast.methods.addToast(res.message, 4000);
+				new Toast({ content: res.message, timeout: 4000 });
 			});
 			});
 		},
 		},
 		...mapActions("modals", ["closeModal"])
 		...mapActions("modals", ["closeModal"])

+ 29 - 6
frontend/components/Modals/IssuesModal.vue

@@ -15,13 +15,26 @@
 			<article class="message">
 			<article class="message">
 				<div class="message-body">
 				<div class="message-body">
 					<strong>Song ID:</strong>
 					<strong>Song ID:</strong>
-					{{ report.songId }}
+					{{ report.song.songId }} / {{ report.song._id }}
 					<br />
 					<br />
-					<strong>Created By:</strong>
-					{{ report.createdBy }}
+					<strong>Author:</strong>
+					<user-id-to-username
+						:userId="report.createdBy"
+						:alt="report.createdBy"
+					/>
 					<br />
 					<br />
-					<strong>Created At:</strong>
-					{{ report.createdAt }}
+					<strong>Time of report:</strong>
+					<span :title="report.createdAt">
+						{{
+							formatDistance(
+								new Date(report.createdAt),
+								new Date(),
+								{
+									addSuffix: true
+								}
+							)
+						}}
+					</span>
 					<br />
 					<br />
 					<span v-if="report.description">
 					<span v-if="report.description">
 						<strong>Description:</strong>
 						<strong>Description:</strong>
@@ -56,6 +69,13 @@
 			>
 			>
 				<span>Resolve</span>
 				<span>Resolve</span>
 			</a>
 			</a>
+			<a
+				class="button is-primary"
+				:href="`/admin/songs?songId=${report.song.songId}`"
+				target="_blank"
+			>
+				<span>Go to song</span>
+			</a>
 			<a
 			<a
 				class="button is-danger"
 				class="button is-danger"
 				@click="
 				@click="
@@ -74,7 +94,9 @@
 
 
 <script>
 <script>
 import { mapActions, mapState } from "vuex";
 import { mapActions, mapState } from "vuex";
+import { formatDistance } from "date-fns";
 
 
+import UserIdToUsername from "../UserIdToUsername.vue";
 import Modal from "./Modal.vue";
 import Modal from "./Modal.vue";
 
 
 export default {
 export default {
@@ -89,9 +111,10 @@ export default {
 		}
 		}
 	},
 	},
 	methods: {
 	methods: {
+		formatDistance,
 		...mapActions("modals", ["closeModal"])
 		...mapActions("modals", ["closeModal"])
 	},
 	},
-	components: { Modal }
+	components: { Modal, UserIdToUsername }
 };
 };
 </script>
 </script>
 
 

+ 37 - 33
frontend/components/Modals/Login.vue

@@ -26,37 +26,39 @@
 			</header>
 			</header>
 			<section class="modal-card-body">
 			<section class="modal-card-body">
 				<!-- validation to check if exists http://bulma.io/documentation/elements/form/ -->
 				<!-- validation to check if exists http://bulma.io/documentation/elements/form/ -->
-				<label class="label">Email</label>
-				<p class="control">
-					<input
-						v-model="email"
-						class="input"
-						type="text"
-						placeholder="Email..."
-					/>
-				</p>
-				<label class="label">Password</label>
-				<p class="control">
-					<input
-						v-model="password"
-						class="input"
-						type="password"
-						placeholder="Password..."
-						@keypress="$parent.submitOnEnter(submitModal, $event)"
-					/>
-				</p>
-				<p>
-					By logging in/registering you agree to our
-					<router-link to="/terms"> Terms of Service </router-link
-					>&nbsp;and
-					<router-link to="/privacy"> Privacy Policy </router-link>.
-				</p>
+				<form>
+					<label class="label">Email</label>
+					<p class="control">
+						<input
+							v-model="email"
+							class="input"
+							type="email"
+							placeholder="Email..."
+						/>
+					</p>
+					<label class="label">Password</label>
+					<p class="control">
+						<input
+							v-model="password"
+							class="input"
+							type="password"
+							placeholder="Password..."
+							@keypress="
+								$parent.submitOnEnter(submitModal, $event)
+							"
+						/>
+					</p>
+					<p>
+						By logging in/registering you agree to our
+						<router-link to="/terms"> Terms of Service </router-link
+						>&nbsp;and
+						<router-link to="/privacy"> Privacy Policy </router-link
+						>.
+					</p>
+				</form>
 			</section>
 			</section>
 			<footer class="modal-card-foot">
 			<footer class="modal-card-foot">
-				<a
-					class="button is-primary"
-					href="#"
-					@click="submitModal('login')"
+				<a class="button is-primary" href="#" @click="submitModal()"
 					>Submit</a
 					>Submit</a
 				>
 				>
 				<a
 				<a
@@ -80,7 +82,7 @@
 <script>
 <script>
 import { mapActions } from "vuex";
 import { mapActions } from "vuex";
 
 
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 
 
 export default {
 export default {
 	data() {
 	data() {
@@ -99,7 +101,9 @@ export default {
 				.then(res => {
 				.then(res => {
 					if (res.status === "success") window.location.reload();
 					if (res.status === "success") window.location.reload();
 				})
 				})
-				.catch(err => Toast.methods.addToast(err.message, 5000));
+				.catch(
+					err => new Toast({ content: err.message, timeout: 5000 })
+				);
 		},
 		},
 		resetPassword() {
 		resetPassword() {
 			this.closeModal({ sector: "header", modal: "login" });
 			this.closeModal({ sector: "header", modal: "login" });
@@ -112,8 +116,8 @@ export default {
 		...mapActions("user/auth", ["login"])
 		...mapActions("user/auth", ["login"])
 	},
 	},
 	mounted() {
 	mounted() {
-		lofig.get("serverDomain", res => {
-			this.serverDomain = res;
+		lofig.get("serverDomain").then(serverDomain => {
+			this.serverDomain = serverDomain;
 		});
 		});
 	}
 	}
 };
 };

+ 13 - 11
frontend/components/Modals/Playlists/Create.vue

@@ -23,7 +23,7 @@
 <script>
 <script>
 import { mapActions } from "vuex";
 import { mapActions } from "vuex";
 
 
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 import Modal from "../Modal.vue";
 import Modal from "../Modal.vue";
 import io from "../../../io";
 import io from "../../../io";
 import validation from "../../../validation";
 import validation from "../../../validation";
@@ -47,18 +47,20 @@ export default {
 		createPlaylist() {
 		createPlaylist() {
 			const { displayName } = this.playlist;
 			const { displayName } = this.playlist;
 			if (!validation.isLength(displayName, 2, 32))
 			if (!validation.isLength(displayName, 2, 32))
-				return Toast.methods.addToast(
-					"Display name must have between 2 and 32 characters.",
-					8000
-				);
-			if (!validation.regex.azAZ09_.test(displayName))
-				return Toast.methods.addToast(
-					"Invalid display name format. Allowed characters: a-z, A-Z, 0-9 and _.",
-					8000
-				);
+				return new Toast({
+					content:
+						"Display name must have between 2 and 32 characters.",
+					timeout: 8000
+				});
+			if (!validation.regex.ascii.test(displayName))
+				return new Toast({
+					content:
+						"Invalid display name format. Only ASCII characters are allowed.",
+					timeout: 8000
+				});
 
 
 			return this.socket.emit("playlists.create", this.playlist, res => {
 			return this.socket.emit("playlists.create", this.playlist, res => {
-				Toast.methods.addToast(res.message, 3000);
+				new Toast({ content: res.message, timeout: 3000 });
 
 
 				if (res.status === "success") {
 				if (res.status === "success") {
 					this.closeModal({
 					this.closeModal({

+ 25 - 22
frontend/components/Modals/Playlists/Edit.vue

@@ -137,7 +137,7 @@
 <script>
 <script>
 import { mapState, mapActions } from "vuex";
 import { mapState, mapActions } from "vuex";
 
 
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 import Modal from "../Modal.vue";
 import Modal from "../Modal.vue";
 import io from "../../../io";
 import io from "../../../io";
 import validation from "../../../validation";
 import validation from "../../../validation";
@@ -276,7 +276,7 @@ export default {
 						});
 						});
 					}
 					}
 				} else if (res.status === "error")
 				} else if (res.status === "error")
-					Toast.methods.addToast(res.message, 3000);
+					new Toast({ content: res.message, timeout: 3000 });
 			});
 			});
 		},
 		},
 		addSongToPlaylist(id) {
 		addSongToPlaylist(id) {
@@ -285,15 +285,16 @@ export default {
 				id,
 				id,
 				this.playlist._id,
 				this.playlist._id,
 				res => {
 				res => {
-					Toast.methods.addToast(res.message, 4000);
+					new Toast({ content: res.message, timeout: 4000 });
 				}
 				}
 			);
 			);
 		},
 		},
 		importPlaylist() {
 		importPlaylist() {
-			Toast.methods.addToast(
-				"Starting to import your playlist. This can take some time to do.",
-				4000
-			);
+			new Toast({
+				content:
+					"Starting to import your playlist. This can take some time to do.",
+				timeout: 4000
+			});
 			this.socket.emit(
 			this.socket.emit(
 				"playlists.addSetToPlaylist",
 				"playlists.addSetToPlaylist",
 				this.importQuery,
 				this.importQuery,
@@ -301,7 +302,7 @@ export default {
 				res => {
 				res => {
 					if (res.status === "success")
 					if (res.status === "success")
 						this.playlist.songs = res.data;
 						this.playlist.songs = res.data;
-					Toast.methods.addToast(res.message, 4000);
+					new Toast({ content: res.message, timeout: 4000 });
 				}
 				}
 			);
 			);
 		},
 		},
@@ -311,35 +312,37 @@ export default {
 				id,
 				id,
 				this.playlist._id,
 				this.playlist._id,
 				res => {
 				res => {
-					Toast.methods.addToast(res.message, 4000);
+					new Toast({ content: res.message, timeout: 4000 });
 				}
 				}
 			);
 			);
 		},
 		},
 		renamePlaylist() {
 		renamePlaylist() {
 			const { displayName } = this.playlist;
 			const { displayName } = this.playlist;
 			if (!validation.isLength(displayName, 2, 32))
 			if (!validation.isLength(displayName, 2, 32))
-				return Toast.methods.addToast(
-					"Display name must have between 2 and 32 characters.",
-					8000
-				);
-			if (!validation.regex.azAZ09_.test(displayName))
-				return Toast.methods.addToast(
-					"Invalid display name format. Allowed characters: a-z, A-Z, 0-9 and _.",
-					8000
-				);
+				return new Toast({
+					content:
+						"Display name must have between 2 and 32 characters.",
+					timeout: 8000
+				});
+			if (!validation.regex.ascii.test(displayName))
+				return new Toast({
+					content:
+						"Invalid display name format. Only ASCII characters are allowed.",
+					timeout: 8000
+				});
 
 
 			return this.socket.emit(
 			return this.socket.emit(
 				"playlists.updateDisplayName",
 				"playlists.updateDisplayName",
 				this.playlist._id,
 				this.playlist._id,
 				this.playlist.displayName,
 				this.playlist.displayName,
 				res => {
 				res => {
-					Toast.methods.addToast(res.message, 4000);
+					new Toast({ content: res.message, timeout: 4000 });
 				}
 				}
 			);
 			);
 		},
 		},
 		removePlaylist() {
 		removePlaylist() {
 			this.socket.emit("playlists.remove", this.playlist._id, res => {
 			this.socket.emit("playlists.remove", this.playlist._id, res => {
-				Toast.methods.addToast(res.message, 3000);
+				new Toast({ content: res.message, timeout: 3000 });
 				if (res.status === "success") {
 				if (res.status === "success") {
 					this.closeModal();
 					this.closeModal();
 				}
 				}
@@ -351,7 +354,7 @@ export default {
 				this.playlist._id,
 				this.playlist._id,
 				songId,
 				songId,
 				res => {
 				res => {
-					Toast.methods.addToast(res.message, 4000);
+					new Toast({ content: res.message, timeout: 4000 });
 				}
 				}
 			);
 			);
 		},
 		},
@@ -361,7 +364,7 @@ export default {
 				this.playlist._id,
 				this.playlist._id,
 				songId,
 				songId,
 				res => {
 				res => {
-					Toast.methods.addToast(res.message, 4000);
+					new Toast({ content: res.message, timeout: 4000 });
 				}
 				}
 			);
 			);
 		},
 		},

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

@@ -31,7 +31,7 @@
 					<input
 					<input
 						v-model="email"
 						v-model="email"
 						class="input"
 						class="input"
-						type="text"
+						type="email"
 						placeholder="Email..."
 						placeholder="Email..."
 						autofocus
 						autofocus
 					/>
 					/>
@@ -84,7 +84,7 @@
 <script>
 <script>
 import { mapActions } from "vuex";
 import { mapActions } from "vuex";
 
 
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 
 
 export default {
 export default {
 	data() {
 	data() {
@@ -100,11 +100,11 @@ export default {
 		};
 		};
 	},
 	},
 	mounted() {
 	mounted() {
-		lofig.get("serverDomain", res => {
-			this.serverDomain = res;
+		lofig.get("serverDomain").then(serverDomain => {
+			this.serverDomain = serverDomain;
 		});
 		});
 
 
-		lofig.get("recaptcha", obj => {
+		lofig.get("recaptcha").then(obj => {
 			this.recaptcha.key = obj.key;
 			this.recaptcha.key = obj.key;
 
 
 			const recaptchaScript = document.createElement("script");
 			const recaptchaScript = document.createElement("script");
@@ -127,8 +127,6 @@ export default {
 	},
 	},
 	methods: {
 	methods: {
 		submitModal() {
 		submitModal() {
-			console.log(this.recaptcha.token);
-
 			this.register({
 			this.register({
 				username: this.username,
 				username: this.username,
 				email: this.email,
 				email: this.email,
@@ -138,7 +136,9 @@ export default {
 				.then(res => {
 				.then(res => {
 					if (res.status === "success") window.location.reload();
 					if (res.status === "success") window.location.reload();
 				})
 				})
-				.catch(err => Toast.methods.addToast(err.message, 5000));
+				.catch(
+					err => new Toast({ content: err.message, timeout: 5000 })
+				);
 		},
 		},
 		githubRedirect() {
 		githubRedirect() {
 			localStorage.setItem("github_redirect", this.$route.path);
 			localStorage.setItem("github_redirect", this.$route.path);

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

@@ -151,7 +151,7 @@
 <script>
 <script>
 import { mapState, mapActions } from "vuex";
 import { mapState, mapActions } from "vuex";
 
 
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 import Modal from "./Modal.vue";
 import Modal from "./Modal.vue";
 import io from "../../io";
 import io from "../../io";
 
 
@@ -222,7 +222,7 @@ export default {
 		create() {
 		create() {
 			console.log(this.report);
 			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);
+				new Toast({ content: res.message, timeout: 4000 });
 				if (res.status === "success")
 				if (res.status === "success")
 					this.closeModal({
 					this.closeModal({
 						sector: "station",
 						sector: "station",

+ 6 - 3
frontend/components/Sidebars/Playlist.vue

@@ -48,7 +48,7 @@
 <script>
 <script>
 import { mapState, mapActions } from "vuex";
 import { mapState, mapActions } from "vuex";
 
 
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 import io from "../../io";
 import io from "../../io";
 
 
 export default {
 export default {
@@ -77,8 +77,11 @@ export default {
 				id,
 				id,
 				res => {
 				res => {
 					if (res.status === "failure")
 					if (res.status === "failure")
-						return Toast.methods.addToast(res.message, 8000);
-					return Toast.methods.addToast(res.message, 4000);
+						return new Toast({
+							content: res.message,
+							timeout: 8000
+						});
+					return new Toast({ content: res.message, timeout: 4000 });
 				}
 				}
 			);
 			);
 		},
 		},

+ 7 - 6
frontend/components/Sidebars/SongsList.vue

@@ -128,7 +128,7 @@
 <script>
 <script>
 import { mapState, mapActions } from "vuex";
 import { mapState, mapActions } from "vuex";
 
 
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 
 
 import UserIdToUsername from "../UserIdToUsername.vue";
 import UserIdToUsername from "../UserIdToUsername.vue";
 
 
@@ -161,11 +161,12 @@ export default {
 				songId,
 				songId,
 				res => {
 				res => {
 					if (res.status === "success") {
 					if (res.status === "success") {
-						Toast.methods.addToast(
-							"Successfully removed song from the queue.",
-							4000
-						);
-					} else Toast.methods.addToast(res.message, 8000);
+						new Toast({
+							content:
+								"Successfully removed song from the queue.",
+							timeout: 4000
+						});
+					} else new Toast({ content: res.message, timeout: 8000 });
 				}
 				}
 			);
 			);
 		},
 		},

+ 123 - 67
frontend/components/Station/Station.vue

@@ -1,14 +1,18 @@
 <template>
 <template>
 	<div>
 	<div>
-		<metadata v-bind:title="`${station.displayName}`" />
+		<metadata
+			v-if="exists && !loading"
+			v-bind:title="`${station.displayName}`"
+		/>
+		<metadata v-else-if="!exists && !loading" v-bind:title="`Not found`" />
 
 
-		<station-header />
+		<station-header v-if="exists" />
 
 
 		<song-queue v-if="modals.addSongToQueue" />
 		<song-queue v-if="modals.addSongToQueue" />
 		<add-to-playlist v-if="modals.addSongToPlaylist" />
 		<add-to-playlist v-if="modals.addSongToPlaylist" />
 		<edit-playlist v-if="modals.editPlaylist" />
 		<edit-playlist v-if="modals.editPlaylist" />
 		<create-playlist v-if="modals.createPlaylist" />
 		<create-playlist v-if="modals.createPlaylist" />
-		<edit-station v-show="modals.editStation" />
+		<edit-station v-show="modals.editStation" store="station" />
 		<report v-if="modals.report" />
 		<report v-if="modals.report" />
 
 
 		<transition name="slide">
 		<transition name="slide">
@@ -418,7 +422,7 @@
 
 
 <script>
 <script>
 import { mapState, mapActions } from "vuex";
 import { mapState, mapActions } from "vuex";
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 
 
 import StationHeader from "./StationHeader.vue";
 import StationHeader from "./StationHeader.vue";
 
 
@@ -453,7 +457,9 @@ export default {
 			systemDifference: 0,
 			systemDifference: 0,
 			attemptsToPlayVideo: 0,
 			attemptsToPlayVideo: 0,
 			canAutoplay: true,
 			canAutoplay: true,
-			lastTimeRequestedIfCanAutoplay: 0
+			lastTimeRequestedIfCanAutoplay: 0,
+			seeking: false,
+			playbackRate: 1
 		};
 		};
 	},
 	},
 	computed: {
 	computed: {
@@ -487,11 +493,12 @@ export default {
 				songId,
 				songId,
 				res => {
 				res => {
 					if (res.status === "success") {
 					if (res.status === "success") {
-						Toast.methods.addToast(
-							"Successfully removed song from the queue.",
-							4000
-						);
-					} else Toast.methods.addToast(res.message, 8000);
+						new Toast({
+							content:
+								"Successfully removed song from the queue.",
+							timeout: 4000
+						});
+					} else new Toast({ content: res.message, timeout: 8000 });
 				}
 				}
 			);
 			);
 		},
 		},
@@ -541,7 +548,7 @@ export default {
 						},
 						},
 						onStateChange: event => {
 						onStateChange: event => {
 							if (
 							if (
-								event.data === 1 &&
+								event.data === window.YT.PlayerState.PLAYING &&
 								this.videoLoading === true
 								this.videoLoading === true
 							) {
 							) {
 								this.videoLoading = false;
 								this.videoLoading = false;
@@ -551,15 +558,23 @@ export default {
 									true
 									true
 								);
 								);
 								if (this.paused) this.player.pauseVideo();
 								if (this.paused) this.player.pauseVideo();
-							} else if (event.data === 1 && this.paused) {
+							} else if (
+								event.data === window.YT.PlayerState.PLAYING &&
+								this.paused
+							) {
 								this.player.seekTo(
 								this.player.seekTo(
 									this.timeBeforePause / 1000,
 									this.timeBeforePause / 1000,
 									true
 									true
 								);
 								);
 								this.player.pauseVideo();
 								this.player.pauseVideo();
+							} else if (
+								event.data === window.YT.PlayerState.PLAYING &&
+								this.seeking === true
+							) {
+								this.seeking = false;
 							}
 							}
 							if (
 							if (
-								event.data === 2 &&
+								event.data === window.YT.PlayerState.PAUSED &&
 								!this.paused &&
 								!this.paused &&
 								!this.noSong &&
 								!this.noSong &&
 								this.player.getDuration() / 1000 <
 								this.player.getDuration() / 1000 <
@@ -667,29 +682,53 @@ export default {
 				const currentPlayerTime = this.player.getCurrentTime() * 1000;
 				const currentPlayerTime = this.player.getCurrentTime() * 1000;
 
 
 				const difference = timeElapsed - currentPlayerTime;
 				const difference = timeElapsed - currentPlayerTime;
-				// console.log(difference123);
-				if (difference < -200) {
+				// console.log(difference);
+
+				let playbackRate = 1;
+
+				if (difference < -2000) {
+					if (!this.seeking) {
+						this.seeking = true;
+						this.player.seekTo(
+							this.getTimeElapsed() / 1000 +
+								this.currentSong.skipDuration
+						);
+					}
+				} else if (difference < -200) {
 					// console.log("Difference0.8");
 					// console.log("Difference0.8");
-					this.player.setPlaybackRate(0.8);
+					playbackRate = 0.8;
 				} else if (difference < -50) {
 				} else if (difference < -50) {
 					// console.log("Difference0.9");
 					// console.log("Difference0.9");
-					this.player.setPlaybackRate(0.9);
+					playbackRate = 0.9;
 				} else if (difference < -25) {
 				} else if (difference < -25) {
 					// console.log("Difference0.99");
 					// console.log("Difference0.99");
-					this.player.setPlaybackRate(0.99);
+					playbackRate = 0.95;
+				} else if (difference > 2000) {
+					if (!this.seeking) {
+						this.seeking = true;
+						this.player.seekTo(
+							this.getTimeElapsed() / 1000 +
+								this.currentSong.skipDuration
+						);
+					}
 				} else if (difference > 200) {
 				} else if (difference > 200) {
 					// console.log("Difference1.2");
 					// console.log("Difference1.2");
-					this.player.setPlaybackRate(1.2);
+					playbackRate = 1.2;
 				} else if (difference > 50) {
 				} else if (difference > 50) {
 					// console.log("Difference1.1");
 					// console.log("Difference1.1");
-					this.player.setPlaybackRate(1.1);
+					playbackRate = 1.1;
 				} else if (difference > 25) {
 				} else if (difference > 25) {
 					// console.log("Difference1.01");
 					// console.log("Difference1.01");
-					this.player.setPlaybackRate(1.01);
+					playbackRate = 1.05;
 				} else if (this.player.getPlaybackRate !== 1.0) {
 				} else if (this.player.getPlaybackRate !== 1.0) {
 					// console.log("NDifference1.0");
 					// console.log("NDifference1.0");
 					this.player.setPlaybackRate(1.0);
 					this.player.setPlaybackRate(1.0);
 				}
 				}
+
+				if (this.playbackRate !== playbackRate) {
+					this.player.setPlaybackRate(playbackRate);
+					this.playbackRate = playbackRate;
+				}
 			}
 			}
 
 
 			/* if (this.currentTime !== undefined && this.paused) {
 			/* if (this.currentTime !== undefined && this.paused) {
@@ -711,11 +750,11 @@ export default {
 		toggleLock() {
 		toggleLock() {
 			window.socket.emit("stations.toggleLock", this.station._id, res => {
 			window.socket.emit("stations.toggleLock", this.station._id, res => {
 				if (res.status === "success") {
 				if (res.status === "success") {
-					Toast.methods.addToast(
-						"Successfully toggled the queue lock.",
-						4000
-					);
-				} else Toast.methods.addToast(res.message, 8000);
+					new Toast({
+						content: "Successfully toggled the queue lock.",
+						timeout: 4000
+					});
+				} else new Toast({ content: res.message, timeout: 8000 });
 			});
 			});
 		},
 		},
 		changeVolume() {
 		changeVolume() {
@@ -752,45 +791,58 @@ export default {
 		skipStation() {
 		skipStation() {
 			this.socket.emit("stations.forceSkip", this.station._id, data => {
 			this.socket.emit("stations.forceSkip", this.station._id, data => {
 				if (data.status !== "success")
 				if (data.status !== "success")
-					Toast.methods.addToast(`Error: ${data.message}`, 8000);
+					new Toast({
+						content: `Error: ${data.message}`,
+						timeout: 8000
+					});
 				else
 				else
-					Toast.methods.addToast(
-						"Successfully skipped the station's current song.",
-						4000
-					);
+					new Toast({
+						content:
+							"Successfully skipped the station's current song.",
+						timeout: 4000
+					});
 			});
 			});
 		},
 		},
 		voteSkipStation() {
 		voteSkipStation() {
 			this.socket.emit("stations.voteSkip", this.station._id, data => {
 			this.socket.emit("stations.voteSkip", this.station._id, data => {
 				if (data.status !== "success")
 				if (data.status !== "success")
-					Toast.methods.addToast(`Error: ${data.message}`, 8000);
+					new Toast({
+						content: `Error: ${data.message}`,
+						timeout: 8000
+					});
 				else
 				else
-					Toast.methods.addToast(
-						"Successfully voted to skip the current song.",
-						4000
-					);
+					new Toast({
+						content: "Successfully voted to skip the current song.",
+						timeout: 4000
+					});
 			});
 			});
 		},
 		},
 		resumeStation() {
 		resumeStation() {
 			this.socket.emit("stations.resume", this.station._id, data => {
 			this.socket.emit("stations.resume", this.station._id, data => {
 				if (data.status !== "success")
 				if (data.status !== "success")
-					Toast.methods.addToast(`Error: ${data.message}`, 8000);
+					new Toast({
+						content: `Error: ${data.message}`,
+						timeout: 8000
+					});
 				else
 				else
-					Toast.methods.addToast(
-						"Successfully resumed the station.",
-						4000
-					);
+					new Toast({
+						content: "Successfully resumed the station.",
+						timeout: 4000
+					});
 			});
 			});
 		},
 		},
 		pauseStation() {
 		pauseStation() {
 			this.socket.emit("stations.pause", this.station._id, data => {
 			this.socket.emit("stations.pause", this.station._id, data => {
 				if (data.status !== "success")
 				if (data.status !== "success")
-					Toast.methods.addToast(`Error: ${data.message}`, 8000);
+					new Toast({
+						content: `Error: ${data.message}`,
+						timeout: 8000
+					});
 				else
 				else
-					Toast.methods.addToast(
-						"Successfully paused the station.",
-						4000
-					);
+					new Toast({
+						content: "Successfully paused the station.",
+						timeout: 4000
+					});
 			});
 			});
 		},
 		},
 		toggleMute() {
 		toggleMute() {
@@ -828,10 +880,10 @@ export default {
 					this.currentSong.songId,
 					this.currentSong.songId,
 					data => {
 					data => {
 						if (data.status !== "success")
 						if (data.status !== "success")
-							Toast.methods.addToast(
-								`Error: ${data.message}`,
-								8000
-							);
+							new Toast({
+								content: `Error: ${data.message}`,
+								timeout: 8000
+							});
 					}
 					}
 				);
 				);
 			else
 			else
@@ -840,10 +892,10 @@ export default {
 					this.currentSong.songId,
 					this.currentSong.songId,
 					data => {
 					data => {
 						if (data.status !== "success")
 						if (data.status !== "success")
-							Toast.methods.addToast(
-								`Error: ${data.message}`,
-								8000
-							);
+							new Toast({
+								content: `Error: ${data.message}`,
+								timeout: 8000
+							});
 					}
 					}
 				);
 				);
 		},
 		},
@@ -854,10 +906,10 @@ export default {
 					this.currentSong.songId,
 					this.currentSong.songId,
 					data => {
 					data => {
 						if (data.status !== "success")
 						if (data.status !== "success")
-							Toast.methods.addToast(
-								`Error: ${data.message}`,
-								8000
-							);
+							new Toast({
+								content: `Error: ${data.message}`,
+								timeout: 8000
+							});
 					}
 					}
 				);
 				);
 
 
@@ -866,7 +918,10 @@ export default {
 				this.currentSong.songId,
 				this.currentSong.songId,
 				data => {
 				data => {
 					if (data.status !== "success")
 					if (data.status !== "success")
-						Toast.methods.addToast(`Error: ${data.message}`, 8000);
+						new Toast({
+							content: `Error: ${data.message}`,
+							timeout: 8000
+						});
 				}
 				}
 			);
 			);
 		},
 		},
@@ -907,10 +962,10 @@ export default {
 										}
 										}
 									);
 									);
 								} else {
 								} else {
-									Toast.methods.addToast(
-										`Top song in playlist was too long to be added.`,
-										3000
-									);
+									new Toast({
+										content: `Top song in playlist was too long to be added.`,
+										timeout: 3000
+									});
 									this.socket.emit(
 									this.socket.emit(
 										"playlists.moveSongToBottom",
 										"playlists.moveSongToBottom",
 										this.privatePlaylistQueueSelected,
 										this.privatePlaylistQueueSelected,
@@ -1010,6 +1065,9 @@ export default {
 						}
 						}
 						this.systemDifference = difference;
 						this.systemDifference = difference;
 					});
 					});
+				} else {
+					this.loading = false;
+					this.exists = false;
 				}
 				}
 			});
 			});
 		},
 		},
@@ -1040,12 +1098,10 @@ export default {
 			io.removeAllListeners();
 			io.removeAllListeners();
 			if (this.socket.connected) this.join();
 			if (this.socket.connected) this.join();
 			io.onConnect(this.join);
 			io.onConnect(this.join);
-			this.socket.emit("stations.findByName", this.stationName, res => {
-				if (res.status === "failure") {
+			this.socket.emit("stations.existsByName", this.stationName, res => {
+				if (res.status === "failure" || !res.exists) {
 					this.loading = false;
 					this.loading = false;
 					this.exists = false;
 					this.exists = false;
-				} else {
-					this.exists = true;
 				}
 				}
 			});
 			});
 			this.socket.on("event:songs.next", data => {
 			this.socket.on("event:songs.next", data => {

+ 9 - 10
frontend/components/Station/StationHeader.vue

@@ -4,7 +4,7 @@
 			<div class="nav-left">
 			<div class="nav-left">
 				<router-link class="nav-item is-brand" :to="{ path: '/' }">
 				<router-link class="nav-item is-brand" :to="{ path: '/' }">
 					<img
 					<img
-						:src="`${this.siteSettings.logo}`"
+						:src="`${this.siteSettings.logo_white}`"
 						:alt="`${this.siteSettings.siteName}` || `Musare`"
 						:alt="`${this.siteSettings.siteName}` || `Musare`"
 					/>
 					/>
 				</router-link>
 				</router-link>
@@ -236,13 +236,12 @@ export default {
 		currentSong: state => state.station.currentSong
 		currentSong: state => state.station.currentSong
 	}),
 	}),
 	mounted() {
 	mounted() {
-		lofig.get("frontendDomain", res => {
-			this.frontendDomain = res;
-			return res;
+		lofig.get("frontendDomain").then(frontendDomain => {
+			this.frontendDomain = frontendDomain;
 		});
 		});
-		lofig.get("siteSettings", res => {
-			this.siteSettings = res;
-			return res;
+
+		lofig.get("siteSettings").then(siteSettings => {
+			this.siteSettings = siteSettings;
 		});
 		});
 	},
 	},
 	methods: {
 	methods: {
@@ -260,7 +259,8 @@ export default {
 				partyMode: this.station.partyMode,
 				partyMode: this.station.partyMode,
 				description: this.station.description,
 				description: this.station.description,
 				privacy: this.station.privacy,
 				privacy: this.station.privacy,
-				displayName: this.station.displayName
+				displayName: this.station.displayName,
+				locked: this.station.locked
 			});
 			});
 			this.openModal({
 			this.openModal({
 				sector: "station",
 				sector: "station",
@@ -286,12 +286,11 @@ export default {
 		font-size: 2.1rem !important;
 		font-size: 2.1rem !important;
 		line-height: 38px !important;
 		line-height: 38px !important;
 		padding: 0 20px;
 		padding: 0 20px;
-		color: $white;
 		font-family: Pacifico, cursive;
 		font-family: Pacifico, cursive;
-		filter: brightness(0) invert(1);
 
 
 		img {
 		img {
 			max-height: 38px;
 			max-height: 38px;
+			color: $musareBlue;
 		}
 		}
 	}
 	}
 }
 }

+ 16 - 7
frontend/components/User/ResetPassword.vue

@@ -69,7 +69,7 @@
 </template>
 </template>
 
 
 <script>
 <script>
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 
 
 import MainHeader from "../MainHeader.vue";
 import MainHeader from "../MainHeader.vue";
 import MainFooter from "../MainFooter.vue";
 import MainFooter from "../MainFooter.vue";
@@ -94,12 +94,15 @@ export default {
 	methods: {
 	methods: {
 		submitEmail() {
 		submitEmail() {
 			if (!this.email)
 			if (!this.email)
-				return Toast.methods.addToast("Email cannot be empty", 8000);
+				return new Toast({
+					content: "Email cannot be empty",
+					timeout: 8000
+				});
 			return this.socket.emit(
 			return this.socket.emit(
 				"users.requestPasswordReset",
 				"users.requestPasswordReset",
 				this.email,
 				this.email,
 				res => {
 				res => {
-					Toast.methods.addToast(res.message, 8000);
+					new Toast({ content: res.message, timeout: 8000 });
 					if (res.status === "success") {
 					if (res.status === "success") {
 						this.step = 2;
 						this.step = 2;
 					}
 					}
@@ -108,12 +111,15 @@ export default {
 		},
 		},
 		verifyCode() {
 		verifyCode() {
 			if (!this.code)
 			if (!this.code)
-				return Toast.methods.addToast("Code cannot be empty", 8000);
+				return new Toast({
+					content: "Code cannot be empty",
+					timeout: 8000
+				});
 			return this.socket.emit(
 			return this.socket.emit(
 				"users.verifyPasswordResetCode",
 				"users.verifyPasswordResetCode",
 				this.code,
 				this.code,
 				res => {
 				res => {
-					Toast.methods.addToast(res.message, 8000);
+					new Toast({ content: res.message, timeout: 8000 });
 					if (res.status === "success") {
 					if (res.status === "success") {
 						this.step = 3;
 						this.step = 3;
 					}
 					}
@@ -122,13 +128,16 @@ export default {
 		},
 		},
 		changePassword() {
 		changePassword() {
 			if (!this.newPassword)
 			if (!this.newPassword)
-				return Toast.methods.addToast("Password cannot be empty", 8000);
+				return new Toast({
+					content: "Password cannot be empty",
+					timeout: 8000
+				});
 			return this.socket.emit(
 			return this.socket.emit(
 				"users.changePasswordWithResetCode",
 				"users.changePasswordWithResetCode",
 				this.code,
 				this.code,
 				this.newPassword,
 				this.newPassword,
 				res => {
 				res => {
-					Toast.methods.addToast(res.message, 8000);
+					new Toast({ content: res.message, timeout: 8000 });
 					if (res.status === "success") {
 					if (res.status === "success") {
 						this.$router.go("/login");
 						this.$router.go("/login");
 					}
 					}

+ 67 - 58
frontend/components/User/Settings.vue

@@ -149,7 +149,7 @@
 <script>
 <script>
 import { mapState } from "vuex";
 import { mapState } from "vuex";
 
 
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 
 
 import MainHeader from "../MainHeader.vue";
 import MainHeader from "../MainHeader.vue";
 import MainFooter from "../MainFooter.vue";
 import MainFooter from "../MainFooter.vue";
@@ -175,8 +175,8 @@ export default {
 		userId: state => state.user.auth.userId
 		userId: state => state.user.auth.userId
 	}),
 	}),
 	mounted() {
 	mounted() {
-		lofig.get("serverDomain", res => {
-			this.serverDomain = res;
+		lofig.get("serverDomain").then(serverDomain => {
+			this.serverDomain = serverDomain;
 		});
 		});
 
 
 		io.getSocket(socket => {
 		io.getSocket(socket => {
@@ -187,10 +187,10 @@ export default {
 					this.password = this.user.password;
 					this.password = this.user.password;
 					this.github = this.user.github;
 					this.github = this.user.github;
 				} else {
 				} else {
-					Toast.methods.addToast(
-						"Your are currently not signed in",
-						3000
-					);
+					new Toast({
+						content: "Your are currently not signed in",
+						timeout: 3000
+					});
 				}
 				}
 			});
 			});
 			this.socket.on("event:user.linkPassword", () => {
 			this.socket.on("event:user.linkPassword", () => {
@@ -211,15 +211,18 @@ export default {
 		changeEmail() {
 		changeEmail() {
 			const email = this.user.email.address;
 			const email = this.user.email.address;
 			if (!validation.isLength(email, 3, 254))
 			if (!validation.isLength(email, 3, 254))
-				return Toast.methods.addToast(
-					"Email must have between 3 and 254 characters.",
-					8000
-				);
+				return new Toast({
+					content: "Email must have between 3 and 254 characters.",
+					timeout: 8000
+				});
 			if (
 			if (
 				email.indexOf("@") !== email.lastIndexOf("@") ||
 				email.indexOf("@") !== email.lastIndexOf("@") ||
 				!validation.regex.emailSimple.test(email)
 				!validation.regex.emailSimple.test(email)
 			)
 			)
-				return Toast.methods.addToast("Invalid email format.", 8000);
+				return new Toast({
+					content: "Invalid email format.",
+					timeout: 8000
+				});
 
 
 			return this.socket.emit(
 			return this.socket.emit(
 				"users.updateEmail",
 				"users.updateEmail",
@@ -227,27 +230,28 @@ export default {
 				email,
 				email,
 				res => {
 				res => {
 					if (res.status !== "success")
 					if (res.status !== "success")
-						Toast.methods.addToast(res.message, 8000);
+						new Toast({ content: res.message, timeout: 8000 });
 					else
 					else
-						Toast.methods.addToast(
-							"Successfully changed email address",
-							4000
-						);
+						new Toast({
+							content: "Successfully changed email address",
+							timeout: 4000
+						});
 				}
 				}
 			);
 			);
 		},
 		},
 		changeUsername() {
 		changeUsername() {
 			const { username } = this.user;
 			const { username } = this.user;
 			if (!validation.isLength(username, 2, 32))
 			if (!validation.isLength(username, 2, 32))
-				return Toast.methods.addToast(
-					"Username must have between 2 and 32 characters.",
-					8000
-				);
+				return new Toast({
+					content: "Username must have between 2 and 32 characters.",
+					timeout: 8000
+				});
 			if (!validation.regex.azAZ09_.test(username))
 			if (!validation.regex.azAZ09_.test(username))
-				return Toast.methods.addToast(
-					"Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _.",
-					8000
-				);
+				return new Toast({
+					content:
+						"Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _.",
+					timeout: 8000
+				});
 
 
 			return this.socket.emit(
 			return this.socket.emit(
 				"users.updateUsername",
 				"users.updateUsername",
@@ -255,45 +259,46 @@ export default {
 				username,
 				username,
 				res => {
 				res => {
 					if (res.status !== "success")
 					if (res.status !== "success")
-						Toast.methods.addToast(res.message, 8000);
+						new Toast({ content: res.message, timeout: 8000 });
 					else
 					else
-						Toast.methods.addToast(
-							"Successfully changed username",
-							4000
-						);
+						new Toast({
+							content: "Successfully changed username",
+							timeout: 4000
+						});
 				}
 				}
 			);
 			);
 		},
 		},
 		changePassword() {
 		changePassword() {
 			const { newPassword } = this;
 			const { newPassword } = this;
 			if (!validation.isLength(newPassword, 6, 200))
 			if (!validation.isLength(newPassword, 6, 200))
-				return Toast.methods.addToast(
-					"Password must have between 6 and 200 characters.",
-					8000
-				);
+				return new Toast({
+					content: "Password must have between 6 and 200 characters.",
+					timeout: 8000
+				});
 			if (!validation.regex.password.test(newPassword))
 			if (!validation.regex.password.test(newPassword))
-				return Toast.methods.addToast(
-					"Invalid password format. Must have one lowercase letter, one uppercase letter, one number and one special character.",
-					8000
-				);
+				return new Toast({
+					content:
+						"Invalid password format. Must have one lowercase letter, one uppercase letter, one number and one special character.",
+					timeout: 8000
+				});
 
 
 			return this.socket.emit(
 			return this.socket.emit(
 				"users.updatePassword",
 				"users.updatePassword",
 				newPassword,
 				newPassword,
 				res => {
 				res => {
 					if (res.status !== "success")
 					if (res.status !== "success")
-						Toast.methods.addToast(res.message, 8000);
+						new Toast({ content: res.message, timeout: 8000 });
 					else
 					else
-						Toast.methods.addToast(
-							"Successfully changed password",
-							4000
-						);
+						new Toast({
+							content: "Successfully changed password",
+							timeout: 4000
+						});
 				}
 				}
 			);
 			);
 		},
 		},
 		requestPassword() {
 		requestPassword() {
 			return this.socket.emit("users.requestPassword", res => {
 			return this.socket.emit("users.requestPassword", res => {
-				Toast.methods.addToast(res.message, 8000);
+				new Toast({ content: res.message, timeout: 8000 });
 				if (res.status === "success") {
 				if (res.status === "success") {
 					this.passwordStep = 2;
 					this.passwordStep = 2;
 				}
 				}
@@ -301,12 +306,15 @@ export default {
 		},
 		},
 		verifyCode() {
 		verifyCode() {
 			if (!this.passwordCode)
 			if (!this.passwordCode)
-				return Toast.methods.addToast("Code cannot be empty", 8000);
+				return new Toast({
+					content: "Code cannot be empty",
+					timeout: 8000
+				});
 			return this.socket.emit(
 			return this.socket.emit(
 				"users.verifyPasswordCode",
 				"users.verifyPasswordCode",
 				this.passwordCode,
 				this.passwordCode,
 				res => {
 				res => {
-					Toast.methods.addToast(res.message, 8000);
+					new Toast({ content: res.message, timeout: 8000 });
 					if (res.status === "success") {
 					if (res.status === "success") {
 						this.passwordStep = 3;
 						this.passwordStep = 3;
 					}
 					}
@@ -316,38 +324,39 @@ export default {
 		setPassword() {
 		setPassword() {
 			const newPassword = this.setNewPassword;
 			const newPassword = this.setNewPassword;
 			if (!validation.isLength(newPassword, 6, 200))
 			if (!validation.isLength(newPassword, 6, 200))
-				return Toast.methods.addToast(
-					"Password must have between 6 and 200 characters.",
-					8000
-				);
+				return new Toast({
+					content: "Password must have between 6 and 200 characters.",
+					timeout: 8000
+				});
 			if (!validation.regex.password.test(newPassword))
 			if (!validation.regex.password.test(newPassword))
-				return Toast.methods.addToast(
-					"Invalid password format. Must have one lowercase letter, one uppercase letter, one number and one special character.",
-					8000
-				);
+				return new Toast({
+					content:
+						"Invalid password format. Must have one lowercase letter, one uppercase letter, one number and one special character.",
+					timeout: 8000
+				});
 
 
 			return this.socket.emit(
 			return this.socket.emit(
 				"users.changePasswordWithCode",
 				"users.changePasswordWithCode",
 				this.passwordCode,
 				this.passwordCode,
 				newPassword,
 				newPassword,
 				res => {
 				res => {
-					Toast.methods.addToast(res.message, 8000);
+					new Toast({ content: res.message, timeout: 8000 });
 				}
 				}
 			);
 			);
 		},
 		},
 		unlinkPassword() {
 		unlinkPassword() {
 			this.socket.emit("users.unlinkPassword", res => {
 			this.socket.emit("users.unlinkPassword", res => {
-				Toast.methods.addToast(res.message, 8000);
+				new Toast({ content: res.message, timeout: 8000 });
 			});
 			});
 		},
 		},
 		unlinkGitHub() {
 		unlinkGitHub() {
 			this.socket.emit("users.unlinkGitHub", res => {
 			this.socket.emit("users.unlinkGitHub", res => {
-				Toast.methods.addToast(res.message, 8000);
+				new Toast({ content: res.message, timeout: 8000 });
 			});
 			});
 		},
 		},
 		removeSessions() {
 		removeSessions() {
 			this.socket.emit(`users.removeSessions`, this.userId, res => {
 			this.socket.emit(`users.removeSessions`, this.userId, res => {
-				Toast.methods.addToast(res.message, 4000);
+				new Toast({ content: res.message, timeout: 4000 });
 			});
 			});
 		}
 		}
 	}
 	}

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

@@ -64,7 +64,7 @@
 
 
 <script>
 <script>
 import { mapState } from "vuex";
 import { mapState } from "vuex";
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 import { format, parseISO } from "date-fns";
 import { format, parseISO } from "date-fns";
 
 
 import MainHeader from "../MainHeader.vue";
 import MainHeader from "../MainHeader.vue";
@@ -111,12 +111,12 @@ export default {
 				newRank === "admin" ? "admin" : "default",
 				newRank === "admin" ? "admin" : "default",
 				res => {
 				res => {
 					if (res.status === "error")
 					if (res.status === "error")
-						Toast.methods.addToast(res.message, 2000);
+						new Toast({ content: res.message, timeout: 2000 });
 					else this.user.role = newRank;
 					else this.user.role = newRank;
-					Toast.methods.addToast(
-						`User ${this.$route.params.username}'s rank has been changed to: ${newRank}`,
-						2000
-					);
+					new Toast({
+						content: `User ${this.$route.params.username}'s rank has been changed to: ${newRank}`,
+						timeout: 2000
+					});
 				}
 				}
 			);
 			);
 		}
 		}

+ 293 - 287
frontend/components/pages/Home.vue

@@ -3,11 +3,11 @@
 		<metadata title="Home" />
 		<metadata title="Home" />
 		<div class="app">
 		<div class="app">
 			<main-header />
 			<main-header />
-			<div class="content-wrapper">
-				<div class="stationsTitle">
+			<div class="group">
+				<div class="group-title">
 					Stations&nbsp;
 					Stations&nbsp;
 					<a
 					<a
-						v-if="loggedIn"
+						v-if="$parent.loggedIn"
 						href="#"
 						href="#"
 						@click="
 						@click="
 							openModal({
 							openModal({
@@ -21,118 +21,100 @@
 						>
 						>
 					</a>
 					</a>
 				</div>
 				</div>
-				<div class="stations">
-					<router-link
-						v-for="(station, index) in filteredStations"
-						:key="index"
-						:to="{
-							name: 'station',
-							params: { id: station.name }
-						}"
-						class="stationCard"
-					>
-						<div class="topContent">
-							<div class="albumArt">
-								<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'"
-								/>
+				<router-link
+					v-for="(station, index) in stations"
+					:key="index"
+					:to="{
+						name: 'station',
+						params: { id: station.name }
+					}"
+					class="card station-card"
+					:class="{
+						isPrivate: station.privacy === 'private',
+						isMine: isOwner(station)
+					}"
+				>
+					<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'"
+							/>
+							<img
+								v-else
+								: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>
+								<i
+									v-if="station.type === 'official'"
+									class="material-icons blue-icon"
+									title="Verified station"
+								>
+									check_circle
+								</i>
 							</div>
 							</div>
-							<div class="info">
-								<h5 class="displayName">
-									{{ station.displayName }}
-									<i
-										v-if="station.type === 'official'"
-										class="badge material-icons"
-									>
-										verified_user
-									</i>
-								</h5>
+						</div>
+
+						<div class="content">
+							{{ station.description }}
+						</div>
+						<div class="under-content">
+							<p v-if="station.type === 'community'">
+								Hosted by
+								<user-id-to-username
+									:userId="station.owner"
+									:link="true"
+								/>
+							</p>
+							<div class="icons">
 								<i
 								<i
-									v-if="loggedIn && !isFavorite(station)"
-									@click="favoriteStation($event, station)"
-									class="favorite material-icons"
-									>star_border</i
+									v-if="isOwner(station)"
+									class="material-icons dark-grey-icon"
+									title="This is your station."
+									>home</i
 								>
 								>
 								<i
 								<i
-									v-if="loggedIn && isFavorite(station)"
-									@click="unfavoriteStation($event, station)"
-									class="favorite material-icons"
-									>star</i
+									v-if="station.privacy !== 'public'"
+									class="material-icons dark-grey-icon"
+									title="This station is not visible to other users."
+									>lock</i
 								>
 								>
-								<p class="description">
-									{{ station.description }}
-								</p>
-								<p 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"
-										/>
-									</span>
-								</p>
-								<div class="bottomIcons">
-									<i
-										v-if="station.privacy !== 'public'"
-										class="privateIcon material-icons"
-										title="This station is not visible to other users."
-										>lock</i
-									>
-									<i
-										v-if="
-											station.type === 'community' &&
-												isOwner(station)
-										"
-										class="homeIcon material-icons"
-										title="This is your station."
-										>home</i
-									>
-								</div>
-							</div>
-						</div>
-						<div class="bottomBar">
-							<i class="material-icons">music_note</i>
-							<span
-								v-if="station.currentSong.title"
-								class="songTitle"
-								>{{ station.currentSong.title }}</span
-							>
-							<span v-else class="songTitle"
-								>No Songs Playing</span
-							>
-							<div class="right">
-								<i class="material-icons">people</i>
-								<span class="currentUsers">{{
-									station.userCount
-								}}</span>
 							</div>
 							</div>
 						</div>
 						</div>
-					</router-link>
-				</div>
+					</div>
+					<div class="bottomBar">
+						<i
+							v-if="station.currentSong.title"
+							class="material-icons"
+							>music_note</i
+						>
+						<i v-else class="material-icons">music_off</i>
+						<span
+							v-if="station.currentSong.title"
+							class="songTitle"
+							:title="'Now Playing: ' + station.currentSong.title"
+							>{{ station.currentSong.title }}</span
+						>
+						<span v-else class="songTitle">No Songs Playing</span>
+					</div>
+				</router-link>
 			</div>
 			</div>
 			<main-footer />
 			<main-footer />
 		</div>
 		</div>
@@ -142,7 +124,7 @@
 
 
 <script>
 <script>
 import { mapState, mapActions } from "vuex";
 import { mapState, mapActions } from "vuex";
-import { Toast } from "vue-roaster";
+import Toast from "toasters";
 
 
 import MainHeader from "../MainHeader.vue";
 import MainHeader from "../MainHeader.vue";
 import MainFooter from "../MainFooter.vue";
 import MainFooter from "../MainFooter.vue";
@@ -195,6 +177,7 @@ export default {
 					station.currentSong.ytThumbnail = `https://img.youtube.com/vi/${station.currentSong.songId}/mqdefault.jpg`;
 					station.currentSong.ytThumbnail = `https://img.youtube.com/vi/${station.currentSong.songId}/mqdefault.jpg`;
 				this.stations.push(station);
 				this.stations.push(station);
 			});
 			});
+
 			this.socket.on(
 			this.socket.on(
 				"event:userCount.updated",
 				"event:userCount.updated",
 				(stationId, userCount) => {
 				(stationId, userCount) => {
@@ -206,6 +189,7 @@ export default {
 					});
 					});
 				}
 				}
 			);
 			);
+
 			this.socket.on("event:station.nextSong", (stationId, song) => {
 			this.socket.on("event:station.nextSong", (stationId, song) => {
 				let newSong = song;
 				let newSong = song;
 				this.stations.forEach(s => {
 				this.stations.forEach(s => {
@@ -221,11 +205,14 @@ export default {
 					}
 					}
 				});
 				});
 			});
 			});
+
 			this.socket.on("event:user.favoritedStation", stationId => {
 			this.socket.on("event:user.favoritedStation", stationId => {
 				this.favoriteStations.push(stationId);
 				this.favoriteStations.push(stationId);
 			});
 			});
+
 			this.socket.on("event:user.unfavoritedStation", stationId => {
 			this.socket.on("event:user.unfavoritedStation", stationId => {
-				this.favoriteStations.$remove(stationId);
+				const index = this.favoriteStations.indexOf(stationId);
+				this.favoriteStations.splice(index, 1);
 			});
 			});
 		});
 		});
 	},
 	},
@@ -255,9 +242,7 @@ export default {
 			this.socket.emit("apis.joinRoom", "home", () => {});
 			this.socket.emit("apis.joinRoom", "home", () => {});
 		},
 		},
 		isOwner(station) {
 		isOwner(station) {
-			return (
-				station.owner === this.userId && station.privacy === "public"
-			);
+			return station.owner === this.userId;
 		},
 		},
 		isFavorite(station) {
 		isFavorite(station) {
 			return this.favoriteStations.indexOf(station._id) !== -1;
 			return this.favoriteStations.indexOf(station._id) !== -1;
@@ -266,22 +251,22 @@ export default {
 			event.preventDefault();
 			event.preventDefault();
 			this.socket.emit("stations.favoriteStation", station._id, res => {
 			this.socket.emit("stations.favoriteStation", station._id, res => {
 				if (res.status === "success") {
 				if (res.status === "success") {
-					Toast.methods.addToast(
-						"Successfully favorited station.",
-						4000
-					);
-				} else Toast.methods.addToast(res.message, 8000);
+					new Toast({
+						content: "Successfully favorited station.",
+						timeout: 4000
+					});
+				} else new Toast({ content: res.message, timeout: 8000 });
 			});
 			});
 		},
 		},
 		unfavoriteStation(event, station) {
 		unfavoriteStation(event, station) {
 			event.preventDefault();
 			event.preventDefault();
 			this.socket.emit("stations.unfavoriteStation", station._id, res => {
 			this.socket.emit("stations.unfavoriteStation", station._id, res => {
 				if (res.status === "success") {
 				if (res.status === "success") {
-					Toast.methods.addToast(
-						"Successfully unfavorited station.",
-						4000
-					);
-				} else Toast.methods.addToast(res.message, 8000);
+					new Toast({
+						content: "Successfully unfavorited station.",
+						timeout: 4000
+					});
+				} else new Toast({ content: res.message, timeout: 8000 });
 			});
 			});
 		},
 		},
 		...mapActions("modals", ["openModal"])
 		...mapActions("modals", ["openModal"])
@@ -305,8 +290,7 @@ export default {
 html {
 html {
 	width: 100%;
 	width: 100%;
 	height: 100%;
 	height: 100%;
-	color: $dark-grey-2;
-
+	color: rgba(0, 0, 0, 0.87);
 	body {
 	body {
 		width: 100%;
 		width: 100%;
 		height: 100%;
 		height: 100%;
@@ -315,213 +299,235 @@ html {
 	}
 	}
 }
 }
 
 
-.stationsTitle {
-	width: 100%;
-	height: 64px;
-	line-height: 48px;
-	text-align: center;
-	font-size: 48px;
-	margin-bottom: 25px;
+@media only screen and (min-width: 1200px) {
+	html {
+		font-size: 15px;
+	}
 }
 }
-.community-button {
-	cursor: pointer;
-	transition: 0.25s ease color;
-	font-size: 30px;
-	color: $dark-grey;
-	&:hover {
-		color: $primary-color;
+
+@media only screen and (min-width: 992px) {
+	html {
+		font-size: 14.5px;
+	}
+}
+
+@media only screen and (min-width: 0) {
+	html {
+		font-size: 14px;
 	}
 	}
 }
 }
 
 
-.stations {
+.under-content {
+	height: 25px;
+	position: relative;
+	line-height: 1;
+	font-size: 24px;
 	display: flex;
 	display: flex;
-	flex: 1;
-	flex-wrap: wrap;
-	justify-content: center;
-	margin-left: 10px;
-	margin-right: 10px;
+	align-items: center;
+	text-align: left;
+	margin-top: 10px;
+
+	p {
+		font-size: 15px;
+		line-height: 15px;
+		display: inline;
+	}
+
+	i {
+		font-size: 20px;
+	}
+
+	* {
+		z-index: 10;
+		position: relative;
+	}
+
+	.icons {
+		position: absolute;
+		right: 0;
+
+		.dark-grey-icon {
+			color: $dark-grey-2;
+		}
+	}
+}
+
+.users-count {
+	font-size: 20px;
+	position: relative;
+	top: -4px;
 }
 }
-.stationCard {
+
+.group {
+	min-height: 64px;
+}
+
+.station-card {
 	display: inline-flex;
 	display: inline-flex;
 	flex-direction: column;
 	flex-direction: column;
-	width: 450px;
-	height: 180px;
-	background: $white;
-	box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1);
-	color: $dark-grey;
+	overflow: hidden;
 	margin: 10px;
 	margin: 10px;
-	transition: all ease-in-out 0.2s;
 	cursor: pointer;
 	cursor: pointer;
-	overflow: hidden;
-	.albumArt {
-		display: inline-flex;
-		position: relative;
-		height: 150px;
-		width: 150px;
-		box-shadow: 1px 0px 3px rgba(7, 136, 191, 0.3);
-		overflow: hidden;
-		img {
-			width: auto;
-			height: 100%;
-		}
-		.ytThumbnailBg {
-			background: url("/assets/notes-transparent.png") no-repeat center
-				center;
-			background-size: cover;
-			height: 100%;
-			width: 100%;
-			position: absolute;
-			top: 0;
-			filter: blur(5px);
-		}
-		.ytThumbnail {
-			height: auto;
-			width: 100%;
-			top: 0;
-			margin-top: auto;
-			margin-bottom: auto;
-			z-index: 1;
-		}
-	}
-	.topContent {
-		width: 100%;
-		height: 100%;
-		display: inline-flex;
-		.info {
-			padding: 15px 12px 12px 15px;
-			position: relative;
-			width: 100%;
-			max-width: 300px;
+	height: 485px;
+	transition: all ease-in-out 0.2s;
+
+	.card-content {
+		padding: 10px 15px;
+
+		.media {
+			display: flex;
+			align-items: center;
+
 			.displayName {
 			.displayName {
-				color: $black;
-				margin: 0;
-				font-size: 20px;
-				font-weight: 500;
-				margin-bottom: 5px;
-				width: calc(100% - 30px);
+				display: flex;
+				align-items: center;
+				word-wrap: break-word;
+				width: 80%;
 				word-wrap: break-word;
 				word-wrap: break-word;
 				overflow: hidden;
 				overflow: hidden;
 				text-overflow: ellipsis;
 				text-overflow: ellipsis;
-				display: -webkit-box;
-				-webkit-box-orient: vertical;
-				-webkit-line-clamp: 1;
+				display: flex;
 				line-height: 30px;
 				line-height: 30px;
 				max-height: 30px;
 				max-height: 30px;
-				.badge {
-					position: relative;
-					padding-right: 2px;
-					color: $lime;
-					top: 3px;
-					font-size: 22px;
-				}
-			}
-			.favorite {
-				color: $yellow;
-				top: 12px;
-				right: 12px;
-				position: absolute;
-				display: none;
-			}
-			.description {
-				width: calc(100% - 30px);
-				margin: 0;
-				font-size: 14px;
-				font-weight: 400;
-				word-wrap: break-word;
-				overflow: hidden;
-				text-overflow: ellipsis;
-				display: -webkit-box;
-				-webkit-box-orient: vertical;
-				-webkit-line-clamp: 3;
-				line-height: 20px;
-				max-height: 60px;
-			}
-			.hostedBy {
-				font-weight: 400;
-				font-size: 12px;
-				position: absolute;
-				bottom: 12px;
-				color: $black;
-				.host {
+
+				h5 {
 					font-weight: 400;
 					font-weight: 400;
-					color: $primary-color;
+					margin: 0;
+					display: inline;
+					margin-right: 6px;
+					line-height: 30px;
 				}
 				}
-			}
-			.bottomIcons {
-				position: absolute;
-				bottom: 12px;
-				right: 12px;
-				.material-icons {
-					margin-left: 5px;
+
+				i {
 					font-size: 22px;
 					font-size: 22px;
 				}
 				}
-				.privateIcon {
-					color: $dark-pink;
-				}
-				.homeIcon {
-					color: $light-purple;
+
+				.blue-icon {
+					color: $musareBlue;
 				}
 				}
 			}
 			}
 		}
 		}
+
+		.content {
+			word-wrap: break-word;
+			overflow: hidden;
+			text-overflow: ellipsis;
+			display: -webkit-box;
+			-webkit-box-orient: vertical;
+			-webkit-line-clamp: 3;
+			line-height: 20px;
+			height: 60px;
+			text-align: left;
+			word-wrap: break-word;
+			margin-bottom: 0;
+		}
 	}
 	}
+
+	.card-image {
+		.image {
+			.ytThumbnailBg {
+				background: url("/assets/notes-transparent.png") no-repeat
+					center center;
+				background-size: cover;
+				height: 100%;
+				width: 100%;
+				position: absolute;
+				top: 0;
+				filter: blur(3px);
+			}
+			img {
+				height: auto;
+				width: 100%;
+				top: 0;
+				margin-top: auto;
+				margin-bottom: auto;
+				z-index: 1;
+			}
+		}
+	}
+
 	.bottomBar {
 	.bottomBar {
+		position: relative;
+		display: flex;
+		align-items: center;
 		background: $primary-color;
 		background: $primary-color;
-		box-shadow: inset 0px 2px 4px rgba(7, 136, 191, 0.6);
 		width: 100%;
 		width: 100%;
 		height: 30px;
 		height: 30px;
 		line-height: 30px;
 		line-height: 30px;
 		color: $white;
 		color: $white;
 		font-weight: 400;
 		font-weight: 400;
 		font-size: 12px;
 		font-size: 12px;
+
 		i.material-icons {
 		i.material-icons {
 			vertical-align: middle;
 			vertical-align: middle;
-			margin-left: 12px;
-			font-size: 22px;
+			margin-left: 5px;
+			font-size: 18px;
 		}
 		}
+
 		.songTitle {
 		.songTitle {
+			text-align: left;
 			vertical-align: middle;
 			vertical-align: middle;
 			margin-left: 5px;
 			margin-left: 5px;
-		}
-		.right {
-			float: right;
-			margin-right: 12px;
-			.currentUsers {
-				vertical-align: middle;
-				margin-left: 5px;
-				font-size: 14px;
-			}
+			line-height: 30px;
+			flex: 2 1 0;
+			overflow: hidden;
+			text-overflow: ellipsis;
+			white-space: nowrap;
 		}
 		}
 	}
 	}
 }
 }
-.stationCard:hover {
+
+.station-card:hover {
 	box-shadow: 0 2px 3px rgba(10, 10, 10, 0.3), 0 0 10px rgba(10, 10, 10, 0.3);
 	box-shadow: 0 2px 3px rgba(10, 10, 10, 0.3), 0 0 10px rgba(10, 10, 10, 0.3);
 	transition: all ease-in-out 0.2s;
 	transition: all ease-in-out 0.2s;
 }
 }
 
 
-@media screen and (max-width: 490px) {
-	.stationCard {
-		width: calc(100% - 20px);
-		height: auto;
-		.topContent {
-			.albumArt {
-				max-height: 100px;
-				max-width: 100px;
-			}
-			.info {
-				width: calc(100% - 100px);
-				padding: 5px 2px 2px 10px !important;
-				.displayName {
-					font-size: 16px !important;
-					margin-bottom: 3px !important;
-				}
-				.description {
-					font-size: 12px !important;
-					-webkit-line-clamp: 2;
-					line-height: 15px;
-					max-height: 30px;
-				}
-			}
-		}
+/*.isPrivate {
+		background-color: #F8BBD0;
+	}
+	.isMine {
+		background-color: #29B6F6;
+	}*/
+
+.community-button {
+	cursor: pointer;
+	transition: 0.25s ease color;
+	font-size: 30px;
+	color: #4a4a4a;
+}
+
+.community-button:hover {
+	color: #03a9f4;
+}
+
+.station-privacy {
+	text-transform: capitalize;
+}
+
+.label {
+	display: flex;
+}
+
+.g-recaptcha {
+	display: flex;
+	justify-content: center;
+	margin-top: 20px;
+}
+
+.group {
+	text-align: center;
+	width: 100%;
+	margin: 64px 0 0 0;
+	padding-bottom: 240px;
+	.group-title {
+		float: left;
+		clear: none;
+		width: 100%;
+		height: 64px;
+		line-height: 48px;
+		text-align: center;
+		font-size: 48px;
+		margin-bottom: 25px;
 	}
 	}
 }
 }
 </style>
 </style>

+ 0 - 0
frontend/dist/assets/wordmark.png → frontend/dist/assets/blue_wordmark.png


BIN
frontend/dist/assets/white_wordmark.png


+ 2 - 1
frontend/dist/config/template.json

@@ -11,7 +11,8 @@
 		"SIDname": "SID"
 		"SIDname": "SID"
 	},
 	},
 	"siteSettings": {
 	"siteSettings": {
-		"logo": "/assets/wordmark.png",
+		"logo_white": "/assets/white_wordmark.png",
+		"logo_blue": "/assets/blue_wordmark.png",
 		"siteName": "Musare",
 		"siteName": "Musare",
 		"socialLinks": {
 		"socialLinks": {
 			"github": "https://github.com/Musare/MusareNode",
 			"github": "https://github.com/Musare/MusareNode",

+ 4 - 1
frontend/dist/index.tpl.html

@@ -39,9 +39,12 @@
 	<script src='https://www.youtube.com/iframe_api'></script>
 	<script src='https://www.youtube.com/iframe_api'></script>
 	<script type='text/javascript' src='/vendor/can-autoplay.min.js'></script>
 	<script type='text/javascript' src='/vendor/can-autoplay.min.js'></script>
 	<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.js" integrity="sha256-yr4fRk/GU1ehYJPAs8P4JlTgu0Hdsp4ZKrx8bDEDC3I=" crossorigin="anonymous"></script>
 	<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.js" integrity="sha256-yr4fRk/GU1ehYJPAs8P4JlTgu0Hdsp4ZKrx8bDEDC3I=" crossorigin="anonymous"></script>
-	<script type='text/javascript' src='/lofig.min.js'></script>
+	<script type='text/javascript' src='https://unpkg.com/lofig@1.2.1/dist/lofig.min.js'></script>
 </head>
 </head>
 <body>
 <body>
 	<div id="root"></div>
 	<div id="root"></div>
+	<div id="toasts-container">
+		<div id="toasts-content"></div>
+	</div>
 </body>
 </body>
 </html>
 </html>

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
frontend/dist/lofig.min.js


+ 16 - 12
frontend/main.js

@@ -27,6 +27,20 @@ Vue.component("metadata", {
 
 
 Vue.use(VueRouter);
 Vue.use(VueRouter);
 
 
+Vue.directive("scroll", {
+	inserted(el, binding) {
+		const f = evt => {
+			clearTimeout(window.scrollDebounceId);
+			window.scrollDebounceId = setTimeout(() => {
+				if (binding.value(evt, el)) {
+					window.removeEventListener("scroll", f);
+				}
+			}, 200);
+		};
+		window.addEventListener("scroll", f);
+	}
+});
+
 const router = new VueRouter({
 const router = new VueRouter({
 	mode: "history",
 	mode: "history",
 	routes: [
 	routes: [
@@ -109,8 +123,8 @@ const router = new VueRouter({
 });
 });
 
 
 lofig.folder = "../config/default.json";
 lofig.folder = "../config/default.json";
-lofig.get("serverDomain", res => {
-	io.init(res);
+lofig.get("serverDomain").then(serverDomain => {
+	io.init(serverDomain);
 	io.getSocket(socket => {
 	io.getSocket(socket => {
 		socket.on("ready", (loggedIn, role, username, userId) => {
 		socket.on("ready", (loggedIn, role, username, userId) => {
 			store.dispatch("user/auth/authData", {
 			store.dispatch("user/auth/authData", {
@@ -156,16 +170,6 @@ router.beforeEach((to, from, next) => {
 			);
 			);
 		}
 		}
 	} else next();
 	} else next();
-
-	if (to.name === "station") {
-		io.getSocket(socket => {
-			socket.emit("stations.findByName", to.params.id, res => {
-				if (res.status === "success") {
-					next();
-				}
-			});
-		});
-	}
 });
 });
 
 
 // eslint-disable-next-line no-new
 // eslint-disable-next-line no-new

+ 1 - 1
frontend/package.json

@@ -50,9 +50,9 @@
     "date-fns": "^2.0.1",
     "date-fns": "^2.0.1",
     "eslint-config-airbnb-base": "^13.2.0",
     "eslint-config-airbnb-base": "^13.2.0",
     "html-webpack-plugin": "^3.2.0",
     "html-webpack-plugin": "^3.2.0",
+    "toasters": "2.0.1",
     "vue": "^2.6.10",
     "vue": "^2.6.10",
     "vue-loader": "^15.7.0",
     "vue-loader": "^15.7.0",
-    "vue-roaster": "^1.1.1",
     "vue-router": "^3.0.7",
     "vue-router": "^3.0.7",
     "vuex": "^3.1.1",
     "vuex": "^3.1.1",
     "webpack-md5-hash": "0.0.6",
     "webpack-md5-hash": "0.0.6",

+ 4 - 2
frontend/validation.js

@@ -3,8 +3,10 @@ module.exports = {
 		azAZ09_: /^[A-Za-z0-9_]+$/,
 		azAZ09_: /^[A-Za-z0-9_]+$/,
 		az09_: /^[a-z0-9_]+$/,
 		az09_: /^[a-z0-9_]+$/,
 		emailSimple: /^[\x00-\x7F]+@[a-z0-9]+\.[a-z0-9]+(\.[a-z0-9]+)?$/,
 		emailSimple: /^[\x00-\x7F]+@[a-z0-9]+\.[a-z0-9]+(\.[a-z0-9]+)?$/,
-		password: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!%*?&])[A-Za-z\d$@$!%*?&]/,
-		ascii: /^[\x00-\x7F]+$/
+		ascii: /^[\x00-\x7F]+$/,
+		custom: regex => {
+			return new RegExp(`^[${regex}]+$`);
+		}
 	},
 	},
 	isLength: (string, min, max) => {
 	isLength: (string, min, max) => {
 		return !(
 		return !(

+ 13 - 146
yarn.lock

@@ -1789,11 +1789,6 @@ acorn-walk@^6.1.1:
   resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c"
   resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c"
   integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==
   integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==
 
 
-acorn@^5.2.1:
-  version "5.7.3"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
-  integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
-
 acorn@^6.0.2, acorn@^6.0.7, acorn@^6.2.1:
 acorn@^6.0.2, acorn@^6.0.7, acorn@^6.2.1:
   version "6.2.1"
   version "6.2.1"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.2.1.tgz#3ed8422d6dec09e6121cc7a843ca86a330a86b51"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.2.1.tgz#3ed8422d6dec09e6121cc7a843ca86a330a86b51"
@@ -2083,11 +2078,6 @@ assign-symbols@^1.0.0:
   resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
   resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
   integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
   integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
 
 
-ast-types@0.9.6:
-  version "0.9.6"
-  resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9"
-  integrity sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=
-
 ast-types@0.x.x:
 ast-types@0.x.x:
   version "0.13.2"
   version "0.13.2"
   resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.2.tgz#df39b677a911a83f3a049644fb74fdded23cea48"
   resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.2.tgz#df39b677a911a83f3a049644fb74fdded23cea48"
@@ -2191,14 +2181,6 @@ babel-plugin-dynamic-import-node@^2.3.0:
   dependencies:
   dependencies:
     object.assign "^4.1.0"
     object.assign "^4.1.0"
 
 
-babel-runtime@^6.0.0:
-  version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
-  integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
-  dependencies:
-    core-js "^2.4.0"
-    regenerator-runtime "^0.11.0"
-
 backo2@1.0.2:
 backo2@1.0.2:
   version "1.0.2"
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
   resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
@@ -2209,11 +2191,6 @@ balanced-match@^1.0.0:
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
   integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
   integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
 
 
-base62@^1.1.0:
-  version "1.2.8"
-  resolved "https://registry.yarnpkg.com/base62/-/base62-1.2.8.tgz#1264cb0fb848d875792877479dbe8bae6bae3428"
-  integrity sha512-V6YHUbjLxN1ymqNLb1DPHoU1CpfdL7d2YTIp5W3U4hhoG4hhxNmsFDs66M9EXxBiSEke5Bt5dwdfMwwZF70iLA==
-
 base64-arraybuffer@0.1.5:
 base64-arraybuffer@0.1.5:
   version "0.1.5"
   version "0.1.5"
   resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
   resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
@@ -2948,7 +2925,7 @@ commander@2.17.x:
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
   integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
   integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
 
 
-commander@^2.18.0, commander@^2.20.0, commander@^2.5.0, commander@~2.20.0:
+commander@^2.18.0, commander@^2.20.0, commander@~2.20.0:
   version "2.20.0"
   version "2.20.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
   integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
   integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
@@ -2963,21 +2940,6 @@ commondir@^1.0.1:
   resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
   resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
   integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
   integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
 
 
-commoner@^0.10.1:
-  version "0.10.8"
-  resolved "https://registry.yarnpkg.com/commoner/-/commoner-0.10.8.tgz#34fc3672cd24393e8bb47e70caa0293811f4f2c5"
-  integrity sha1-NPw2cs0kOT6LtH5wyqApOBH08sU=
-  dependencies:
-    commander "^2.5.0"
-    detective "^4.3.1"
-    glob "^5.0.15"
-    graceful-fs "^4.1.2"
-    iconv-lite "^0.4.5"
-    mkdirp "^0.5.0"
-    private "^0.1.6"
-    q "^1.1.2"
-    recast "^0.11.17"
-
 compare-func@^1.3.1:
 compare-func@^1.3.1:
   version "1.3.2"
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/compare-func/-/compare-func-1.3.2.tgz#99dd0ba457e1f9bc722b12c08ec33eeab31fa648"
   resolved "https://registry.yarnpkg.com/compare-func/-/compare-func-1.3.2.tgz#99dd0ba457e1f9bc722b12c08ec33eeab31fa648"
@@ -3283,11 +3245,6 @@ core-js-pure@3.1.4:
   resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.1.4.tgz#5fa17dc77002a169a3566cc48dc774d2e13e3769"
   resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.1.4.tgz#5fa17dc77002a169a3566cc48dc774d2e13e3769"
   integrity sha512-uJ4Z7iPNwiu1foygbcZYJsJs1jiXrTTCvxfLDXNhI/I+NHbSIEyr548y4fcsCEyWY0XgfAG/qqaunJ1SThHenA==
   integrity sha512-uJ4Z7iPNwiu1foygbcZYJsJs1jiXrTTCvxfLDXNhI/I+NHbSIEyr548y4fcsCEyWY0XgfAG/qqaunJ1SThHenA==
 
 
-core-js@^2.4.0:
-  version "2.6.9"
-  resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2"
-  integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==
-
 core-util-is@1.0.2, core-util-is@~1.0.0:
 core-util-is@1.0.2, core-util-is@~1.0.0:
   version "1.0.2"
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
   resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@@ -3632,11 +3589,6 @@ define-property@^2.0.2:
     is-descriptor "^1.0.2"
     is-descriptor "^1.0.2"
     isobject "^3.0.1"
     isobject "^3.0.1"
 
 
-defined@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
-  integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=
-
 degenerator@^1.0.4:
 degenerator@^1.0.4:
   version "1.0.4"
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-1.0.4.tgz#fcf490a37ece266464d9cc431ab98c5819ced095"
   resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-1.0.4.tgz#fcf490a37ece266464d9cc431ab98c5819ced095"
@@ -3712,14 +3664,6 @@ detect-node@^2.0.4:
   resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
   resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
   integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==
   integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==
 
 
-detective@^4.3.1:
-  version "4.7.1"
-  resolved "https://registry.yarnpkg.com/detective/-/detective-4.7.1.tgz#0eca7314338442febb6d65da54c10bb1c82b246e"
-  integrity sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig==
-  dependencies:
-    acorn "^5.2.1"
-    defined "^1.0.0"
-
 dezalgo@^1.0.0:
 dezalgo@^1.0.0:
   version "1.0.3"
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456"
   resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456"
@@ -4028,14 +3972,6 @@ env-paths@^1.0.0:
   resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0"
   resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0"
   integrity sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=
   integrity sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=
 
 
-envify@^3.4.0:
-  version "3.4.1"
-  resolved "https://registry.yarnpkg.com/envify/-/envify-3.4.1.tgz#d7122329e8df1688ba771b12501917c9ce5cbce8"
-  integrity sha1-1xIjKejfFoi6dxsSUBkXyc5cvOg=
-  dependencies:
-    jstransform "^11.0.3"
-    through "~2.3.4"
-
 err-code@^1.0.0:
 err-code@^1.0.0:
   version "1.1.2"
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960"
   resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960"
@@ -4281,12 +4217,7 @@ espree@^6.0.0:
     acorn-jsx "^5.0.0"
     acorn-jsx "^5.0.0"
     eslint-visitor-keys "^1.0.0"
     eslint-visitor-keys "^1.0.0"
 
 
-esprima-fb@^15001.1.0-dev-harmony-fb:
-  version "15001.1.0-dev-harmony-fb"
-  resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-15001.1.0-dev-harmony-fb.tgz#30a947303c6b8d5e955bee2b99b1d233206a6901"
-  integrity sha1-MKlHMDxrjV6VW+4rmbHSMyBqaQE=
-
-esprima@3.x.x, esprima@^3.1.3, esprima@~3.1.0:
+esprima@3.x.x, esprima@^3.1.3:
   version "3.1.3"
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
   integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=
   integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=
@@ -4989,17 +4920,6 @@ glob-to-regexp@^0.3.0:
   resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
   resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
   integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
   integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
 
 
-glob@^5.0.15:
-  version "5.0.15"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
-  integrity sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=
-  dependencies:
-    inflight "^1.0.4"
-    inherits "2"
-    minimatch "2 || 3"
-    once "^1.3.0"
-    path-is-absolute "^1.0.0"
-
 glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@~7.1.1:
 glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@~7.1.1:
   version "7.1.4"
   version "7.1.4"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
@@ -5481,7 +5401,7 @@ husky@^3.0.4:
     run-node "^1.0.0"
     run-node "^1.0.0"
     slash "^3.0.0"
     slash "^3.0.0"
 
 
-iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@^0.4.5, iconv-lite@~0.4.13:
+iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
   version "0.4.24"
   version "0.4.24"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
   integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
   integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@@ -6031,16 +5951,6 @@ isstream@~0.1.2:
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
   integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
   integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
 
 
-jquery-ui@^1.12.1:
-  version "1.12.1"
-  resolved "https://registry.yarnpkg.com/jquery-ui/-/jquery-ui-1.12.1.tgz#bcb4045c8dd0539c134bc1488cdd3e768a7a9e51"
-  integrity sha1-vLQEXI3QU5wTS8FIjN0+dop6nlE=
-
-jquery@^3.1.1:
-  version "3.4.1"
-  resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.1.tgz#714f1f8d9dde4bdfa55764ba37ef214630d80ef2"
-  integrity sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==
-
 js-base64@^2.1.8:
 js-base64@^2.1.8:
   version "2.5.1"
   version "2.5.1"
   resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.1.tgz#1efa39ef2c5f7980bb1784ade4a8af2de3291121"
   resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.1.tgz#1efa39ef2c5f7980bb1784ade4a8af2de3291121"
@@ -6160,17 +6070,6 @@ jsprim@^1.2.2:
     json-schema "0.2.3"
     json-schema "0.2.3"
     verror "1.10.0"
     verror "1.10.0"
 
 
-jstransform@^11.0.3:
-  version "11.0.3"
-  resolved "https://registry.yarnpkg.com/jstransform/-/jstransform-11.0.3.tgz#09a78993e0ae4d4ef4487f6155a91f6190cb4223"
-  integrity sha1-CaeJk+CuTU70SH9hVakfYZDLQiM=
-  dependencies:
-    base62 "^1.1.0"
-    commoner "^0.10.1"
-    esprima-fb "^15001.1.0-dev-harmony-fb"
-    object-assign "^2.0.0"
-    source-map "^0.4.2"
-
 jszip@^3.1.5:
 jszip@^3.1.5:
   version "3.2.2"
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.2.2.tgz#b143816df7e106a9597a94c77493385adca5bd1d"
   resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.2.2.tgz#b143816df7e106a9597a94c77493385adca5bd1d"
@@ -6793,7 +6692,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
   resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
   resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
   integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
   integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
 
 
-"minimatch@2 || 3", minimatch@^3.0.4, minimatch@~3.0.2:
+minimatch@^3.0.4, minimatch@~3.0.2:
   version "3.0.4"
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
   integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
   integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
@@ -7376,11 +7275,6 @@ oauth@^0.9.15:
   resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
   resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
   integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE=
   integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE=
 
 
-object-assign@^2.0.0:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-2.1.1.tgz#43c36e5d569ff8e4816c4efa8be02d26967c18aa"
-  integrity sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=
-
 object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
 object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
   version "4.1.1"
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@@ -8155,7 +8049,7 @@ prism-media@^0.0.3:
   resolved "https://registry.yarnpkg.com/prism-media/-/prism-media-0.0.3.tgz#8842d4fae804f099d3b48a9a38e3c2bab6f4855b"
   resolved "https://registry.yarnpkg.com/prism-media/-/prism-media-0.0.3.tgz#8842d4fae804f099d3b48a9a38e3c2bab6f4855b"
   integrity sha512-c9KkNifSMU/iXT8FFTaBwBMr+rdVcN+H/uNv1o+CuFeTThNZNTOrQ+RgXA1yL/DeLk098duAeRPP3QNPNbhxYQ==
   integrity sha512-c9KkNifSMU/iXT8FFTaBwBMr+rdVcN+H/uNv1o+CuFeTThNZNTOrQ+RgXA1yL/DeLk098duAeRPP3QNPNbhxYQ==
 
 
-private@^0.1.6, private@~0.1.5:
+private@^0.1.6:
   version "0.1.8"
   version "0.1.8"
   resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
   resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
   integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
   integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
@@ -8320,7 +8214,7 @@ punycode@^2.1.0:
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
 
 
-q@^1.1.2, q@^1.5.1:
+q@^1.5.1:
   version "1.5.1"
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
   resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
   integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
   integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
@@ -8552,16 +8446,6 @@ readdirp@^2.2.1:
     micromatch "^3.1.10"
     micromatch "^3.1.10"
     readable-stream "^2.0.2"
     readable-stream "^2.0.2"
 
 
-recast@^0.11.17:
-  version "0.11.23"
-  resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3"
-  integrity sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM=
-  dependencies:
-    ast-types "0.9.6"
-    esprima "~3.1.0"
-    private "~0.1.5"
-    source-map "~0.5.0"
-
 redent@^1.0.0:
 redent@^1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
   resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
@@ -8609,11 +8493,6 @@ regenerate@^1.4.0:
   resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
   resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
   integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==
   integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==
 
 
-regenerator-runtime@^0.11.0:
-  version "0.11.1"
-  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
-  integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
-
 regenerator-runtime@^0.13.2:
 regenerator-runtime@^0.13.2:
   version "0.13.3"
   version "0.13.3"
   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"
   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"
@@ -9614,7 +9493,7 @@ source-map@^0.4.2:
   dependencies:
   dependencies:
     amdefine ">=0.0.4"
     amdefine ">=0.0.4"
 
 
-source-map@^0.5.0, source-map@^0.5.6, source-map@~0.5.0:
+source-map@^0.5.0, source-map@^0.5.6:
   version "0.5.7"
   version "0.5.7"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
   integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
   integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
@@ -10061,7 +9940,7 @@ through2@^3.0.0:
   dependencies:
   dependencies:
     readable-stream "2 || 3"
     readable-stream "2 || 3"
 
 
-through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@~2.3.4:
+through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6:
   version "2.3.8"
   version "2.3.8"
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
   integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
   integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
@@ -10147,6 +10026,11 @@ to-regex@^3.0.1, to-regex@^3.0.2:
     regex-not "^1.0.2"
     regex-not "^1.0.2"
     safe-regex "^1.1.0"
     safe-regex "^1.1.0"
 
 
+toasters@2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/toasters/-/toasters-2.0.1.tgz#a41c2e1652e51b9b782624027672363ed6547509"
+  integrity sha512-zqZz0itO/iQJhnLKkTGpj1FhWg0YKo2EnAGD0zjDPrR2rQgfSymCaHaQT0Gp9VIuPpu+3UY8m4w4HTI49HpdFw==
+
 toidentifier@1.0.0:
 toidentifier@1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
   resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
@@ -10610,16 +10494,6 @@ vue-loader@^15.7.0:
     vue-hot-reload-api "^2.3.0"
     vue-hot-reload-api "^2.3.0"
     vue-style-loader "^4.1.0"
     vue-style-loader "^4.1.0"
 
 
-vue-roaster@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/vue-roaster/-/vue-roaster-1.1.1.tgz#4718300137862e621e8d7f9130c6dacb5989ca1a"
-  integrity sha1-RxgwATeGLmIejX+RMMbay1mJyho=
-  dependencies:
-    babel-runtime "^6.0.0"
-    jquery "^3.1.1"
-    jquery-ui "^1.12.1"
-    vue "^1.0.0"
-
 vue-router@^3.0.7:
 vue-router@^3.0.7:
   version "3.1.1"
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.1.1.tgz#0893c29548ba2dbe35ed104dcd1aa06743aa0ead"
   resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.1.1.tgz#0893c29548ba2dbe35ed104dcd1aa06743aa0ead"
@@ -10646,13 +10520,6 @@ vue-template-es2015-compiler@^1.9.0:
   resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
   resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
   integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
   integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
 
 
-vue@^1.0.0:
-  version "1.0.28"
-  resolved "https://registry.yarnpkg.com/vue/-/vue-1.0.28.tgz#ed2ff07b200bde15c87a90ef8727ceea7d38567d"
-  integrity sha1-7S/weyAL3hXIepDvhyfO6n04Vn0=
-  dependencies:
-    envify "^3.4.0"
-
 vue@^2.6.10:
 vue@^2.6.10:
   version "2.6.10"
   version "2.6.10"
   resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637"
   resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637"

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.