소스 검색

refactor: Provide application config over WS and split frontend config into backend and .env

Owen Diffey 2 년 전
부모
커밋
d8b73be1de
56개의 변경된 파일635개의 추가작업 그리고 1467개의 파일을 삭제
  1. 14 2
      .env.example
  2. 2 2
      .github/workflows/automated-tests.yml
  3. 2 2
      .github/workflows/build-lint.yml
  4. 1 2
      .gitignore
  5. 2 2
      backend/Dockerfile
  6. 20 0
      backend/config/custom-environment-variables.json
  7. 52 57
      backend/config/default.json
  8. 1 1
      backend/index.js
  9. 1 1
      backend/logic/api.js
  10. 22 8
      backend/logic/app.js
  11. 1 1
      backend/logic/db/index.js
  12. 6 6
      backend/logic/mail/index.js
  13. 4 3
      backend/logic/mail/schemas/dataRequest.js
  14. 2 3
      backend/logic/mail/schemas/verifyEmail.js
  15. 36 17
      backend/logic/ws.js
  16. 21 4
      docker-compose.yml
  17. 0 1
      frontend/.eslintrc
  18. 2 4
      frontend/Dockerfile
  19. 0 56
      frontend/dist/config/template.json
  20. 4 4
      frontend/entrypoint.sh
  21. 11 763
      frontend/package-lock.json
  22. 1 3
      frontend/package.json
  23. 62 70
      frontend/src/App.vue
  24. 9 7
      frontend/src/classes/SocketHandler.class.ts
  25. 18 26
      frontend/src/components/MainFooter.vue
  26. 11 18
      frontend/src/components/MainHeader.vue
  27. 4 2
      frontend/src/components/Modal.spec.ts
  28. 8 8
      frontend/src/components/Modal.vue
  29. 5 8
      frontend/src/components/PlaylistItem.vue
  30. 4 2
      frontend/src/components/ProfilePicture.vue
  31. 6 16
      frontend/src/components/Request.vue
  32. 7 20
      frontend/src/components/modals/EditPlaylist/Tabs/AddSongs.vue
  33. 3 5
      frontend/src/components/modals/EditPlaylist/index.vue
  34. 6 6
      frontend/src/components/modals/EditSong/Tabs/Songs.vue
  35. 3 16
      frontend/src/components/modals/EditSong/Tabs/Youtube.vue
  36. 5 5
      frontend/src/components/modals/EditSong/index.vue
  37. 6 14
      frontend/src/components/modals/Login.vue
  38. 1 1
      frontend/src/components/modals/ManageStation/index.vue
  39. 31 46
      frontend/src/components/modals/Register.vue
  40. 18 21
      frontend/src/components/modals/RemoveAccount.vue
  41. 6 7
      frontend/src/keyboardShortcuts.ts
  42. 6 40
      frontend/src/main.ts
  43. 0 6
      frontend/src/pages/Admin/index.vue
  44. 17 16
      frontend/src/pages/Home.vue
  45. 13 15
      frontend/src/pages/Settings/Tabs/Security.vue
  46. 5 5
      frontend/src/pages/Station/Sidebar/Users.vue
  47. 15 22
      frontend/src/pages/Station/index.vue
  48. 70 0
      frontend/src/stores/config.ts
  49. 6 6
      frontend/src/stores/modals.ts
  50. 49 47
      frontend/src/stores/userAuth.ts
  51. 18 20
      frontend/src/stores/websockets.ts
  52. 0 1
      frontend/src/tests/utils/setup.ts
  53. 0 16
      frontend/src/tests/utils/utils.ts
  54. 0 5
      frontend/src/types/global.d.ts
  55. 15 21
      frontend/vite.config.js
  56. 3 7
      musare.sh

+ 14 - 2
.env.example

@@ -1,6 +1,6 @@
 COMPOSE_PROJECT_NAME=musare
 RESTART_POLICY=unless-stopped
-CONTAINER_MODE=prod
+CONTAINER_MODE=production
 DOCKER_COMMAND=docker
 
 BACKEND_HOST=127.0.0.1
@@ -8,7 +8,10 @@ BACKEND_PORT=8080
 
 FRONTEND_HOST=127.0.0.1
 FRONTEND_PORT=80
-FRONTEND_MODE=prod
+FRONTEND_CLIENT_PORT=80
+FRONTEND_DEV_PORT=81
+FRONTEND_MODE=production
+FRONTEND_PROD_DEVTOOLS=false
 
 MONGO_HOST=127.0.0.1
 MONGO_PORT=27017
@@ -25,3 +28,12 @@ REDIS_DATA_LOCATION=.redis
 
 BACKUP_LOCATION=
 BACKUP_NAME=
+
+MUSARE_SITENAME=Musare
+
+MUSARE_DEBUG_VERSION=true
+MUSARE_DEBUG_GIT_REMOTE=false
+MUSARE_DEBUG_GIT_REMOTE_URL=false
+MUSARE_DEBUG_GIT_BRANCH=true
+MUSARE_DEBUG_GIT_LATEST_COMMIT=true
+MUSARE_DEBUG_GIT_LATEST_COMMIT_SHORT=true

+ 2 - 2
.github/workflows/automated-tests.yml

@@ -5,12 +5,12 @@ on: [ push, pull_request, workflow_dispatch ]
 env:
     COMPOSE_PROJECT_NAME: musare
     RESTART_POLICY: unless-stopped
-    CONTAINER_MODE: prod
+    CONTAINER_MODE: production
     BACKEND_HOST: 127.0.0.1
     BACKEND_PORT: 8080
     FRONTEND_HOST: 127.0.0.1
     FRONTEND_PORT: 80
-    FRONTEND_MODE: prod
+    FRONTEND_MODE: production
     MONGO_HOST: 127.0.0.1
     MONGO_PORT: 27017
     MONGO_ROOT_PASSWORD: PASSWORD_HERE

+ 2 - 2
.github/workflows/build-lint.yml

@@ -5,12 +5,12 @@ on: [ push, pull_request, workflow_dispatch ]
 env:
     COMPOSE_PROJECT_NAME: musare
     RESTART_POLICY: unless-stopped
-    CONTAINER_MODE: prod
+    CONTAINER_MODE: production
     BACKEND_HOST: 127.0.0.1
     BACKEND_PORT: 8080
     FRONTEND_HOST: 127.0.0.1
     FRONTEND_PORT: 80
-    FRONTEND_MODE: prod
+    FRONTEND_MODE: production
     MONGO_HOST: 127.0.0.1
     MONGO_PORT: 27017
     MONGO_ROOT_PASSWORD: PASSWORD_HERE

+ 1 - 2
.gitignore

@@ -20,7 +20,7 @@ lerna-debug.log
 
 # Backend
 backend/node_modules/
-backend/config/default.json
+backend/config/local*.json
 backend/build
 
 # Frontend
@@ -28,7 +28,6 @@ frontend/bundle-stats.json
 frontend/bundle-report.html
 frontend/node_modules/
 frontend/build/
-frontend/dist/config/default.json
 frontend/src/coverage/
 
 npm

+ 2 - 2
backend/Dockerfile

@@ -10,7 +10,7 @@ RUN npm install --silent
 
 FROM node:18 AS musare_backend
 
-ARG CONTAINER_MODE=prod
+ARG CONTAINER_MODE=production
 ENV CONTAINER_MODE=${CONTAINER_MODE}
 
 RUN mkdir -p /opt/app /opt/types
@@ -19,7 +19,7 @@ WORKDIR /opt/app
 COPY . /opt/app
 COPY --from=backend_node_modules /opt/app/node_modules node_modules
 
-ENTRYPOINT bash -c '([[ "${CONTAINER_MODE}" == "dev" ]] && npm install --silent); npm run docker:dev'
+ENTRYPOINT bash -c '([[ "${CONTAINER_MODE}" == "development" ]] && npm install --silent); npm run docker:dev'
 
 EXPOSE 8080/tcp
 EXPOSE 8080/udp

+ 20 - 0
backend/config/custom-environment-variables.json

@@ -0,0 +1,20 @@
+{
+	"sitename": "MUSARE_SITENAME",
+	"redis": {
+		"url": "REDIS_URL",
+		"password": "REDIS_PASSWORD"
+	},
+	"mongo": {
+		"url": "MONGO_URL"
+	},
+	"debug": {
+		"git": {
+			"remote": "MUSARE_DEBUG_GIT_REMOTE",
+			"remoteUrl": "MUSARE_DEBUG_GIT_REMOTE_URL",
+			"branch": "MUSARE_DEBUG_GIT_BRANCH",
+			"latestCommit": "MUSARE_DEBUG_GIT_LATEST_COMMIT",
+			"latestCommitShort": "MUSARE_DEBUG_GIT_LATEST_COMMIT_SHORT"
+		},
+		"version": "MUSARE_DEBUG_VERSION"
+	}
+}

+ 52 - 57
backend/config/template.json → backend/config/default.json

@@ -1,13 +1,13 @@
 {
-	"mode": "development",
 	"migration": false,
 	"secret": "default",
-	"domain": "http://localhost",
-	"frontendPort": 80,
-	"serverDomain": "http://localhost/backend",
-	"serverPort": 8080,
-	"registrationDisabled": true,
-	"sendDataRequestEmails": true,
+	"port": 8080,
+	"url": {
+		"host": "localhost",
+		"secure": false
+	},
+	"cookie": "musareSID",
+	"sitename": "Musare",
 	"apis": {
 		"youtube": {
 			"key": "",
@@ -46,8 +46,9 @@
 			"retryAmount": 2
 		},
 		"recaptcha": {
-			"secret": "",
-			"enabled": false
+			"enabled": false,
+			"key": "",
+			"secret": ""
 		},
 		"github": {
 			"enabled": false,
@@ -56,28 +57,26 @@
 			"redirect_uri": ""
 		},
 		"discogs": {
+			"enabled": false,
 			"client": "",
-			"secret": "",
-			"enabled": false
+			"secret": ""
 		}
 	},
 	"cors": {
-		"origin": [
-			"http://localhost"
-		]
-	},
-	"smtp": {
-		"host": "smtp.mailgun.org",
-		"port": 587,
-		"auth": {
-			"user": "",
-			"pass": ""
-		},
-		"secure": false,
-		"enabled": false
+		"origin": ["http://localhost"]
 	},
 	"mail": {
-		"from": "Musare <noreply@localhost>"
+		"from": "Musare <noreply@localhost>",
+		"smtp": {
+			"enabled": false,
+			"host": "smtp.mailgun.org",
+			"port": 587,
+			"auth": {
+				"user": "",
+				"pass": ""
+			},
+			"secure": false
+		}
 	},
 	"redis": {
 		"url": "redis://redis:6379/0",
@@ -86,34 +85,36 @@
 	"mongo": {
 		"url": "mongodb://musare:OTHER_PASSWORD_HERE@mongo:27017/musare"
 	},
-	"cookie": {
-		"domain": "localhost",
-		"secure": false,
-		"SIDname": "SID"
-	},
-	"blacklistedCommunityStationNames": [
-		"musare"
-	],
+	"blacklistedCommunityStationNames": ["musare"],
 	"featuredPlaylists": [],
+	"messages": {
+		"accountRemoval": "Your account will be deactivated instantly and your data will shortly be deleted by an admin."
+	},
+	"christmas": false,
+	"footerLinks": {},
+	"shortcutOverrides": {},
+	"registrationDisabled": false,
+	"sendDataRequestEmails": true,
 	"skipConfigVersionCheck": false,
 	"skipDbDocumentsVersionCheck": false,
 	"debug": {
 		"stationIssue": false,
 		"traceUnhandledPromises": false,
-		"captureJobs": []
+		"captureJobs": [],
+		"git": {
+			"remote": false,
+			"remoteUrl": false,
+			"branch": true,
+			"latestCommit": true,
+			"latestCommitShort": true
+		},
+		"version": true
 	},
 	"defaultLogging": {
-		"hideType": [
-			"INFO"
-		],
+		"hideType": ["INFO"],
 		"blacklistedTerms": []
 	},
 	"customLoggingPerModule": {
-		// "cache": {
-		//     "hideType": [
-		//     ],
-		//     "blacklistedTerms": []
-		// },
 		"migration": {
 			"hideType": [],
 			"blacklistedTerms": [
@@ -127,20 +128,14 @@
 			]
 		}
 	},
-	"configVersion": 11,
 	"experimental": {
-		"weight_stations": {
-			"STATION_ID": true,
-			"STATION_ID_2": "alternative_weight"
-		},
-		"queue_autofill_skip_last_x_played": {
-			"STATION_ID": 5,
-			"STATION_ID_2": 10
-		},
-		"queue_add_before_autofilled": [
-			"STATION_ID"
-		],
+		"weight_stations": {},
+		"queue_autofill_skip_last_x_played": {},
+		"queue_add_before_autofilled": [],
 		"disable_youtube_search": false,
-		"registration_email_whitelist": false
-	}
-}
+		"registration_email_whitelist": false,
+		"changable_listen_mode": [],
+		"media_session": false
+	},
+	"configVersion": 12
+}

+ 1 - 1
backend/index.js

@@ -6,7 +6,7 @@ import fs from "fs";
 
 import package_json from "./package.json" assert { type: "json" };
 
-const REQUIRED_CONFIG_VERSION = 11;
+const REQUIRED_CONFIG_VERSION = 12;
 
 // eslint-disable-next-line
 Array.prototype.remove = function (item) {

+ 1 - 1
backend/logic/api.js

@@ -36,7 +36,7 @@ class _APIModule extends CoreClass {
 			CacheModule = this.moduleManager.modules.cache;
 			NotificationsModule = this.moduleManager.modules.notifications;
 
-			const SIDname = config.get("cookie.SIDname");
+			const SIDname = config.get("cookie");
 
 			const isLoggedIn = (req, res, next) => {
 				let SID;

+ 22 - 8
backend/logic/app.js

@@ -42,8 +42,8 @@ class _AppModule extends CoreClass {
 			UtilsModule = this.moduleManager.modules.utils;
 
 			const app = (this.app = express());
-			const SIDname = config.get("cookie.SIDname");
-			this.server = http.createServer(app).listen(config.get("serverPort"));
+			const SIDname = config.get("cookie");
+			this.server = http.createServer(app).listen(config.get("port"));
 
 			app.use(cookieParser());
 
@@ -67,7 +67,11 @@ class _AppModule extends CoreClass {
 			 * @param {string} err - custom error message
 			 */
 			function redirectOnErr(res, err) {
-				res.redirect(`${config.get("domain")}?err=${encodeURIComponent(err)}`);
+				res.redirect(
+					`${config.get("url.secure") ? "https" : "http"}://${config.get(
+						"url.host"
+					)}?err=${encodeURIComponent(err)}`
+				);
 			}
 
 			if (config.get("apis.github.enabled")) {
@@ -222,7 +226,11 @@ class _AppModule extends CoreClass {
 													value: { userId: user._id }
 												});
 
-												res.redirect(`${config.get("domain")}/settings?tab=security`);
+												res.redirect(
+													`${config.get("url.secure") ? "https" : "http"}://${config.get(
+														"url.host"
+													)}/settings?tab=security`
+												);
 											}
 										],
 										next
@@ -428,9 +436,9 @@ class _AppModule extends CoreClass {
 
 									res.cookie(SIDname, sessionId, {
 										expires: date,
-										secure: config.get("cookie.secure"),
+										secure: config.get("url.secure"),
 										path: "/",
-										domain: config.get("cookie.domain")
+										domain: config.get("url.host")
 									});
 
 									this.log(
@@ -439,7 +447,9 @@ class _AppModule extends CoreClass {
 										`User "${userId}" successfully authorized with GitHub.`
 									);
 
-									res.redirect(`${config.get("domain")}/`);
+									res.redirect(
+										`${config.get("url.secure") ? "https" : "http"}://${config.get("url.host")}/`
+									);
 								})
 								.catch(err => redirectOnErr(res, err.message));
 						}
@@ -502,7 +512,11 @@ class _AppModule extends CoreClass {
 
 						this.log("INFO", "VERIFY_EMAIL", `Successfully verified email.`);
 
-						return res.redirect(`${config.get("domain")}?toast=Thank you for verifying your email`);
+						return res.redirect(
+							`${config.get("url.secure") ? "https" : "http"}://${config.get(
+								"url.host"
+							)}?toast=Thank you for verifying your email`
+						);
 					}
 				);
 			});

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

@@ -266,7 +266,7 @@ class _DBModule extends CoreClass {
 
 					const songThumbnail = thumbnail => {
 						if (!isLength(thumbnail, 1, 256)) return false;
-						if (config.get("cookie.secure") === true) return thumbnail.startsWith("https://");
+						if (config.get("url.secure") === true) return thumbnail.startsWith("https://");
 						return thumbnail.startsWith("http://") || thumbnail.startsWith("https://");
 					};
 					this.schemas.song.path("thumbnail").validate(songThumbnail, "Invalid thumbnail.");

+ 6 - 6
backend/logic/mail/index.js

@@ -32,16 +32,16 @@ class _MailModule extends CoreClass {
 			dataRequest: await importSchema("dataRequest")
 		};
 
-		this.enabled = config.get("smtp.enabled");
+		this.enabled = config.get("mail.smtp.enabled");
 
 		if (this.enabled)
 			this.transporter = nodemailer.createTransport({
-				host: config.get("smtp.host"),
-				port: config.get("smtp.port"),
-				secure: config.get("smtp.secure"),
+				host: config.get("mail.smtp.host"),
+				port: config.get("mail.smtp.port"),
+				secure: config.get("mail.smtp.secure"),
 				auth: {
-					user: config.get("smtp.auth.user"),
-					pass: config.get("smtp.auth.pass")
+					user: config.get("mail.smtp.auth.user"),
+					pass: config.get("mail.smtp.auth.pass")
 				}
 			});
 

+ 4 - 3
backend/logic/mail/schemas/dataRequest.js

@@ -22,9 +22,10 @@ export default (to, userId, type, cb) => {
 				User ${userId} has requested to ${type} the data for their account on Musare.
 				<br>
 				<br>
-				This request can be viewed and resolved in the <a href="${config.get(
-					"domain"
-				)}/admin/users">Users tab of the admin page</a>. Note: All admins will be sent the same message.
+				This request can be viewed and resolved in the
+				<a href="${config.get("url.secure") ? "https" : "http"}://${config.get(
+			"url.host"
+		)}/admin/users">Users tab of the admin page</a>. Note: All admins will be sent the same message.
 			`
 	};
 

+ 2 - 3
backend/logic/mail/schemas/verifyEmail.js

@@ -10,6 +10,7 @@ import mail from "../index";
  * @param {Function} cb - gets called when an error occurred or when the operation was successful
  */
 export default (to, username, code, cb) => {
+	const url = `${config.get("url.secure") ? "https" : "http"}://${config.get("url.host")}/backend`;
 	const data = {
 		from: config.get("mail.from"),
 		to,
@@ -18,9 +19,7 @@ export default (to, username, code, cb) => {
 				Hello there ${username},
 				<br>
 				<br>
-				To verify your email, please visit <a href="${config.get("serverDomain")}/auth/verify_email?code=${code}">${config.get(
-			"serverDomain"
-		)}/auth/verify_email?code=${code}</a>.
+				To verify your email, please visit <a href="${url}/auth/verify_email?code=${code}">${url}/auth/verify_email?code=${code}</a>.
 			`
 	};
 

+ 36 - 17
backend/logic/ws.js

@@ -44,7 +44,7 @@ class _WSModule extends CoreClass {
 
 		this.setStage(2);
 
-		this.SIDname = config.get("cookie.SIDname");
+		this.SIDname = config.get("cookie");
 
 		// TODO: Check every 30s/, for all sockets, if they are still allowed to be in the rooms they are in, and on socket at all (permission changing/banning)
 		const server = await AppModule.runJob("SERVER");
@@ -586,6 +586,29 @@ class _WSModule extends CoreClass {
 				console.error("SOCKET ERROR: ", error);
 			};
 
+			const readyData = {
+				config: {
+					cookie: config.get("cookie"),
+					sitename: config.get("sitename"),
+					recaptcha: {
+						enabled: config.get("apis.recaptcha.enabled"),
+						key: config.get("apis.recaptcha.key")
+					},
+					githubAuthentication: config.get("apis.github.enabled"),
+					messages: config.get("messages"),
+					christmas: config.get("christmas"),
+					footerLinks: config.get("footerLinks"),
+					shortcutOverrides: config.get("shortcutOverrides"),
+					registrationDisabled: config.get("registrationDisabled"),
+					experimental: {
+						changable_listen_mode: config.get("experimental.changable_listen_mode"),
+						media_session: config.get("experimental.media_session"),
+						disable_youtube_search: config.get("experimental.disable_youtube_search")
+					}
+				},
+				user: { loggedIn: false }
+			};
+
 			if (socket.session.sessionId) {
 				CacheModule.runJob("HGET", {
 					table: "sessions",
@@ -594,28 +617,24 @@ class _WSModule extends CoreClass {
 					.then(session => {
 						if (session && session.userId) {
 							WSModule.userModel.findOne({ _id: session.userId }, (err, user) => {
-								if (err || !user) return socket.dispatch("ready", { data: { loggedIn: false } });
-
-								let role = "";
-								let username = "";
-								let userId = "";
-								let email = "";
+								if (err || !user) return socket.dispatch("ready", readyData);
 
 								if (user) {
-									role = user.role;
-									username = user.username;
-									email = user.email.address;
-									userId = session.userId;
+									readyData.user = {
+										loggedIn: true,
+										role: user.role,
+										username: user.username,
+										email: user.email.address,
+										userId: session.userId
+									};
 								}
 
-								return socket.dispatch("ready", {
-									data: { loggedIn: true, role, username, userId, email }
-								});
+								return socket.dispatch("ready", readyData);
 							});
-						} else socket.dispatch("ready", { data: { loggedIn: false } });
+						} else socket.dispatch("ready", readyData);
 					})
-					.catch(() => socket.dispatch("ready", { data: { loggedIn: false } }));
-			} else socket.dispatch("ready", { data: { loggedIn: false } });
+					.catch(() => socket.dispatch("ready", readyData));
+			} else socket.dispatch("ready", readyData);
 
 			socket.onmessage = message => {
 				const data = JSON.parse(message.data);

+ 21 - 4
docker-compose.yml

@@ -11,7 +11,14 @@ services:
       - ./backend/config:/opt/app/config
       - ./types:/opt/types
     environment:
-      - CONTAINER_MODE=${CONTAINER_MODE:-prod}
+      - CONTAINER_MODE=${CONTAINER_MODE:-production}
+      - MUSARE_SITENAME=${MUSARE_SITENAME:-Musare}
+      - MUSARE_DEBUG_VERSION=${MUSARE_DEBUG_VERSION:-true}
+      - MUSARE_DEBUG_GIT_REMOTE=${MUSARE_DEBUG_GIT_REMOTE:-false}
+      - MUSARE_DEBUG_GIT_REMOTE_URL=${MUSARE_DEBUG_GIT_REMOTE_URL:-false}
+      - MUSARE_DEBUG_GIT_BRANCH=${MUSARE_DEBUG_GIT_BRANCH:-true}
+      - MUSARE_DEBUG_GIT_LATEST_COMMIT=${MUSARE_DEBUG_GIT_LATEST_COMMIT:-true}
+      - MUSARE_DEBUG_GIT_LATEST_COMMIT_SHORT=${MUSARE_DEBUG_GIT_LATEST_COMMIT_SHORT:-true}
     links:
       - mongo
       - redis
@@ -23,7 +30,7 @@ services:
       context: ./frontend
       target: musare_frontend
       args:
-        FRONTEND_MODE: "${FRONTEND_MODE:-prod}"
+        FRONTEND_MODE: "${FRONTEND_MODE:-production}"
     restart: ${RESTART_POLICY:-unless-stopped}
     user: root
     ports:
@@ -33,8 +40,18 @@ services:
       - ./frontend/dist/config:/opt/app/dist/config
       - ./types:/opt/types
     environment:
-      - FRONTEND_MODE=${FRONTEND_MODE:-prod}
-      - CONTAINER_MODE=${CONTAINER_MODE:-prod}
+      - CONTAINER_MODE=${CONTAINER_MODE:-production}
+      - FRONTEND_MODE=${FRONTEND_MODE:-production}
+      - FRONTEND_PORT=${FRONTEND_PORT:-80}
+      - FRONTEND_CLIENT_PORT=${FRONTEND_CLIENT_PORT:-80}
+      - FRONTEND_DEV_PORT=${FRONTEND_DEV_PORT:-81}
+      - MUSARE_SITENAME=${MUSARE_SITENAME:-Musare}
+      - MUSARE_DEBUG_VERSION=${MUSARE_DEBUG_VERSION:-true}
+      - MUSARE_DEBUG_GIT_REMOTE=${MUSARE_DEBUG_GIT_REMOTE:-false}
+      - MUSARE_DEBUG_GIT_REMOTE_URL=${MUSARE_DEBUG_GIT_REMOTE_URL:-false}
+      - MUSARE_DEBUG_GIT_BRANCH=${MUSARE_DEBUG_GIT_BRANCH:-true}
+      - MUSARE_DEBUG_GIT_LATEST_COMMIT=${MUSARE_DEBUG_GIT_LATEST_COMMIT:-true}
+      - MUSARE_DEBUG_GIT_LATEST_COMMIT_SHORT=${MUSARE_DEBUG_GIT_LATEST_COMMIT_SHORT:-true}
     links:
       - backend
 

+ 0 - 1
frontend/.eslintrc

@@ -26,7 +26,6 @@
 		"@typescript-eslint"
 	],
 	"globals": {
-		"lofig": "writable",
 		"grecaptcha": "readonly",
 		"history": "readonly"
 	},

+ 2 - 4
frontend/Dockerfile

@@ -10,10 +10,8 @@ RUN npm install --silent
 
 FROM node:18 AS musare_frontend
 
-ARG FRONTEND_MODE=prod
+ARG FRONTEND_MODE=production
 ENV FRONTEND_MODE=${FRONTEND_MODE}
-ENV SUPPRESS_NO_CONFIG_WARNING=1
-ENV NODE_CONFIG_DIR=./dist/config
 
 RUN apt update
 RUN apt install nginx -y
@@ -26,7 +24,7 @@ COPY --from=frontend_node_modules /opt/app/node_modules node_modules
 
 RUN mkdir -p /run/nginx
 
-RUN bash -c '([[ "${FRONTEND_MODE}" == "dev" ]] && exit 0) || npm run prod'
+RUN bash -c '([[ "${FRONTEND_MODE}" == "development" ]] && exit 0) || npm run prod'
 
 RUN chmod u+x entrypoint.sh
 

+ 0 - 56
frontend/dist/config/template.json

@@ -1,56 +0,0 @@
-{
-	"recaptcha": {
-		"key": "",
-		"enabled": false
-	},
-	"backend": {
-		"apiDomain": "http://localhost/backend",
-		"websocketsDomain": "ws://localhost/backend/ws"
-	},
-	"devServer": {
-		"port": 81,
-		"hmrClientPort": 80
-	},
-	"frontendDomain": "http://localhost",
-	"mode": "development",
-	"cookie": {
-		"domain": "localhost",
-		"secure": false,
-		"SIDname": "SID"
-	},
-	"siteSettings": {
-		"logo_white": "/assets/white_wordmark.png",
-		"logo_blue": "/assets/blue_wordmark.png",
-		"logo_small": "/assets/favicon/mstile-144x144.png",
-		"sitename": "Musare",
-		"footerLinks": {
-			"GitHub": "https://github.com/Musare/Musare"
-		},
-		"christmas": false,
-		"registrationDisabled": false,
-		"githubAuthentication": false
-	},
-	"messages": {
-		"accountRemoval": "Your account will be deactivated instantly and your data will shortly be deleted by an admin."
-	},
-	"shortcutOverrides": {},
-	"debug": {
-		"git": {
-			"remote": false,
-			"remoteUrl": false,
-			"branch": true,
-			"latestCommit": true,
-			"latestCommitShort": true
-		},
-		"version": true
-	},
-	"skipConfigVersionCheck": false,
-	"configVersion": 13,
-	"experimental": {
-		"changable_listen_mode": [
-			"STATION_ID"
-		],
-		"disable_youtube_search": true,
-		"media_session": false
-	}
-}

+ 4 - 4
frontend/entrypoint.sh

@@ -1,15 +1,15 @@
 #!/bin/bash
 
-if [[ "${CONTAINER_MODE}" == "dev" ]]; then
+if [[ "${CONTAINER_MODE}" == "development" ]]; then
     npm install --silent
 fi
 
-if [[ "${FRONTEND_MODE}" == "prod" ]]; then
-    if [[ "${CONTAINER_MODE}" == "dev" ]]; then
+if [[ "${FRONTEND_MODE}" == "production" ]]; then
+    if [[ "${CONTAINER_MODE}" == "development" ]]; then
         npm run prod
     fi
     nginx -c /opt/app/prod.nginx.conf -g "daemon off;"
-elif [ "${FRONTEND_MODE}" == "dev" ]; then
+elif [ "${FRONTEND_MODE}" == "development" ]; then
     nginx -c /opt/app/dev.nginx.conf
     npm run dev
 fi

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 11 - 763
frontend/package-lock.json


+ 1 - 3
frontend/package.json

@@ -46,11 +46,9 @@
     "@vitejs/plugin-vue": "^4.0.0",
     "can-autoplay": "^3.0.2",
     "chart.js": "^4.2.1",
-    "config": "^3.3.9",
     "date-fns": "^2.29.3",
     "dompurify": "^3.0.1",
     "eslint-config-airbnb-base": "^15.0.0",
-    "lofig": "^1.3.4",
     "marked": "^4.2.12",
     "normalize.css": "^8.0.1",
     "pinia": "^2.0.33",
@@ -66,4 +64,4 @@
     "vue-router": "^4.1.6",
     "vue-tippy": "^6.0.0"
   }
-}
+}

+ 62 - 70
frontend/src/App.vue

@@ -7,6 +7,7 @@ import { GenericResponse } from "@musare_types/actions/GenericActions";
 import { GetPreferencesResponse } from "@musare_types/actions/UsersActions";
 import { NewestResponse } from "@musare_types/actions/NewsActions";
 import { useWebsocketsStore } from "@/stores/websockets";
+import { useConfigStore } from "@/stores/config";
 import { useUserAuthStore } from "@/stores/userAuth";
 import { useUserPreferencesStore } from "@/stores/userPreferences";
 import { useModalsStore } from "@/stores/modals";
@@ -27,11 +28,11 @@ const FallingSnow = defineAsyncComponent(
 const router = useRouter();
 
 const { socket } = useWebsocketsStore();
+const configStore = useConfigStore();
 const userAuthStore = useUserAuthStore();
 const userPreferencesStore = useUserPreferencesStore();
 const modalsStore = useModalsStore();
 
-const apiDomain = ref("");
 const socketConnected = ref(true);
 const keyIsDown = ref("");
 const broadcastChannel = ref({
@@ -102,10 +103,20 @@ onMounted(async () => {
 			if (e.matches === !nightmode.value) changeNightmode(true);
 		});
 
-	if (!loggedIn.value) {
-		lofig.get("cookie.SIDname").then(sid => {
+	disconnectedMessage.value = new Toast({
+		content: "Could not connect to the server.",
+		persistent: true,
+		interactable: false
+	});
+
+	disconnectedMessage.value.hide();
+
+	socket.onConnect(() => {
+		socketConnected.value = true;
+
+		if (!loggedIn.value) {
 			broadcastChannel.value.user_login = new BroadcastChannel(
-				`${sid}.user_login`
+				`${configStore.get("cookie")}.user_login`
 			);
 			broadcastChannel.value.user_login.onmessage = res => {
 				if (res.data) {
@@ -115,61 +126,49 @@ onMounted(async () => {
 			};
 
 			broadcastChannel.value.nightmode = new BroadcastChannel(
-				`${sid}.nightmode`
+				`${configStore.get("cookie")}.nightmode`
 			);
 			broadcastChannel.value.nightmode.onmessage = res => {
 				changeNightmode(!!res.data);
 			};
-		});
-	}
-
-	document.onkeydown = (ev: KeyboardEvent) => {
-		const event = ev || window.event;
-		const { keyCode } = event;
-		const shift = event.shiftKey;
-		const ctrl = event.ctrlKey;
-		const alt = event.altKey;
-
-		const identifier = `${keyCode}.${shift}.${ctrl}`;
-
-		if (keyIsDown.value === identifier) return;
-		keyIsDown.value = identifier;
+		}
 
-		keyboardShortcuts.handleKeyDown(event, keyCode, shift, ctrl, alt);
-	};
+		document.onkeydown = (ev: KeyboardEvent) => {
+			const event = ev || window.event;
+			const { keyCode } = event;
+			const shift = event.shiftKey;
+			const ctrl = event.ctrlKey;
+			const alt = event.altKey;
 
-	document.onkeyup = () => {
-		keyIsDown.value = "";
-	};
+			const identifier = `${keyCode}.${shift}.${ctrl}`;
 
-	// ctrl + alt + n
-	keyboardShortcuts.registerShortcut("nightmode", {
-		keyCode: 78,
-		ctrl: true,
-		alt: true,
-		handler: () => toggleNightMode()
-	});
+			if (keyIsDown.value === identifier) return;
+			keyIsDown.value = identifier;
 
-	keyboardShortcuts.registerShortcut("closeModal", {
-		keyCode: 27,
-		shift: false,
-		ctrl: false,
-		handler: () => {
-			if (Object.keys(activeModals.value).length !== 0)
-				closeCurrentModal();
-		}
-	});
+			keyboardShortcuts.handleKeyDown(event, keyCode, shift, ctrl, alt);
+		};
 
-	disconnectedMessage.value = new Toast({
-		content: "Could not connect to the server.",
-		persistent: true,
-		interactable: false
-	});
+		document.onkeyup = () => {
+			keyIsDown.value = "";
+		};
 
-	disconnectedMessage.value.hide();
+		// ctrl + alt + n
+		keyboardShortcuts.registerShortcut("nightmode", {
+			keyCode: 78,
+			ctrl: true,
+			alt: true,
+			handler: () => toggleNightMode()
+		});
 
-	socket.onConnect(() => {
-		socketConnected.value = true;
+		keyboardShortcuts.registerShortcut("closeModal", {
+			keyCode: 27,
+			shift: false,
+			ctrl: false,
+			handler: () => {
+				if (Object.keys(activeModals.value).length !== 0)
+					closeCurrentModal();
+			}
+		});
 
 		socket.dispatch(
 			"users.getPreferences",
@@ -232,6 +231,21 @@ onMounted(async () => {
 			if (!localStorage.getItem("firstVisited"))
 				localStorage.setItem("firstVisited", Date.now().toString());
 		});
+
+		router.isReady().then(() => {
+			if (
+				configStore.get("githubAuthentication") &&
+				localStorage.getItem("github_redirect")
+			) {
+				router.push(localStorage.getItem("github_redirect") as string);
+				localStorage.removeItem("github_redirect");
+			}
+		});
+
+		if (configStore.get("christmas")) {
+			christmas.value = true;
+			enableChristmasMode();
+		}
 	}, true);
 
 	socket.onDisconnect(() => {
@@ -242,31 +256,9 @@ onMounted(async () => {
 		window.location.reload()
 	);
 
-	apiDomain.value = await lofig.get("backend.apiDomain");
-
-	router.isReady().then(() => {
-		lofig
-			.get("siteSettings.githubAuthentication")
-			.then((enabled: boolean) => {
-				if (enabled && localStorage.getItem("github_redirect")) {
-					router.push(
-						localStorage.getItem("github_redirect") as string
-					);
-					localStorage.removeItem("github_redirect");
-				}
-			});
-	});
-
 	if (localStorage.getItem("nightmode") === "true") {
 		changeNightmode(true);
 	} else changeNightmode(false);
-
-	lofig.get("siteSettings.christmas").then((enabled: boolean) => {
-		if (enabled) {
-			christmas.value = true;
-			enableChristmasMode();
-		}
-	});
 });
 </script>
 

+ 9 - 7
frontend/src/classes/SocketHandler.class.ts

@@ -1,4 +1,5 @@
 import ListenerHandler from "@/classes/ListenerHandler.class";
+import { useConfigStore } from "@/stores/config";
 import { useUserAuthStore } from "@/stores/userAuth";
 import utils from "@/utils";
 
@@ -39,11 +40,9 @@ export default class SocketHandler {
 
 	trigger: (type: string, target: string, data?: any) => void; // Mock only
 
-	constructor(url: string) {
+	constructor() {
 		this.dispatcher = new ListenerHandler();
 
-		this.url = url;
-
 		this.onConnectCbs = {
 			temp: [],
 			persist: []
@@ -72,10 +71,11 @@ export default class SocketHandler {
 	}
 
 	init() {
-		this.socket = new WebSocket(this.url);
-
+		const configStore = useConfigStore();
 		const userAuthStore = useUserAuthStore();
 
+		this.socket = new WebSocket(configStore.urls.ws);
+
 		this.socket.onopen = () => {
 			console.log("WS: SOCKET OPENED");
 		};
@@ -122,8 +122,10 @@ export default class SocketHandler {
 
 		if (this.firstInit) {
 			this.firstInit = false;
-			this.on("ready", () => {
-				console.log("WS: SOCKET READY");
+			this.on("ready", data => {
+				console.log("WS: SOCKET READY", data);
+
+				configStore.setConfig(data.config);
 
 				this.onConnectCbs.temp.forEach(cb => cb());
 				this.onConnectCbs.persist.forEach(cb => cb());

+ 18 - 26
frontend/src/components/MainFooter.vue

@@ -1,15 +1,14 @@
 <script setup lang="ts">
 import { ref, computed, onMounted } from "vue";
+import { useConfigStore } from "@/stores/config";
 
-const siteSettings = ref({
-	logo_blue: "/assets/blue_wordmark.png",
-	sitename: "Musare",
-	footerLinks: {}
-});
+const footerLinks = ref({});
+
+const configStore = useConfigStore();
 
 const filteredFooterLinks = computed(() =>
 	Object.fromEntries(
-		Object.entries(siteSettings.value.footerLinks).filter(
+		Object.entries(footerLinks.value).filter(
 			([title, url]) =>
 				!(
 					["about", "team", "news"].includes(title.toLowerCase()) &&
@@ -20,24 +19,17 @@ const filteredFooterLinks = computed(() =>
 );
 
 const getLink = title =>
-	siteSettings.value.footerLinks[
-		Object.keys(siteSettings.value.footerLinks).find(
-			key => key.toLowerCase() === title
-		)
+	footerLinks.value[
+		Object.keys(footerLinks.value).find(key => key.toLowerCase() === title)
 	];
 
-onMounted(async () => {
-	lofig.get("siteSettings").then(settings => {
-		siteSettings.value = {
-			...settings,
-			footerLinks: {
-				about: true,
-				team: true,
-				news: true,
-				...settings.footerLinks
-			}
-		};
-	});
+onMounted(() => {
+	footerLinks.value = {
+		about: true,
+		team: true,
+		news: true,
+		...configStore.get("footerLinks")
+	};
 });
 </script>
 
@@ -50,11 +42,11 @@ onMounted(async () => {
 				</div>
 				<router-link id="footer-logo" to="/">
 					<img
-						v-if="siteSettings.sitename === 'Musare'"
-						:src="siteSettings.logo_blue"
-						:alt="siteSettings.sitename || `Musare`"
+						v-if="configStore.get('sitename') === 'Musare'"
+						src="/assets/blue_wordmark.png"
+						:alt="configStore.get('sitename')"
 					/>
-					<span v-else>{{ siteSettings.sitename }}</span>
+					<span v-else>{{ configStore.get("sitename") }}</span>
 				</router-link>
 				<div id="footer-links">
 					<a

+ 11 - 18
frontend/src/components/MainHeader.vue

@@ -3,6 +3,7 @@ import { defineAsyncComponent, ref, onMounted, watch, nextTick } from "vue";
 import Toast from "toasters";
 import { storeToRefs } from "pinia";
 import { useWebsocketsStore } from "@/stores/websockets";
+import { useConfigStore } from "@/stores/config";
 import { useUserAuthStore } from "@/stores/userAuth";
 import { useUserPreferencesStore } from "@/stores/userPreferences";
 import { useModalsStore } from "@/stores/modals";
@@ -21,19 +22,12 @@ const userAuthStore = useUserAuthStore();
 
 const localNightmode = ref(false);
 const isMobile = ref(false);
-const frontendDomain = ref("");
-const siteSettings = ref({
-	logo_white: "/assets/white_wordmark.png",
-	sitename: "Musare",
-	christmas: false,
-	registrationDisabled: false
-});
 const windowWidth = ref(0);
-const sidName = ref();
 const broadcastChannel = ref();
 
 const { socket } = useWebsocketsStore();
 
+const configStore = useConfigStore();
 const { loggedIn, username } = storeToRefs(userAuthStore);
 const { logout, hasPermission } = userAuthStore;
 const userPreferencesStore = useUserPreferencesStore();
@@ -68,15 +62,14 @@ watch(nightmode, value => {
 
 onMounted(async () => {
 	localNightmode.value = nightmode.value;
-	frontendDomain.value = await lofig.get("frontendDomain");
-	siteSettings.value = await lofig.get("siteSettings");
-	sidName.value = await lofig.get("cookie.SIDname");
 
 	await nextTick();
 	onResize();
 	window.addEventListener("resize", onResize);
 
-	broadcastChannel.value = new BroadcastChannel(`${sidName.value}.nightmode`);
+	broadcastChannel.value = new BroadcastChannel(
+		`${configStore.get("cookie")}.nightmode`
+	);
 });
 </script>
 
@@ -88,11 +81,11 @@ onMounted(async () => {
 		<div class="nav-left">
 			<router-link v-if="!hideLogo" class="nav-item is-brand" to="/">
 				<img
-					v-if="siteSettings.sitename === 'Musare'"
-					:src="siteSettings.logo_white"
-					:alt="siteSettings.sitename || `Musare`"
+					v-if="configStore.get('sitename') === 'Musare'"
+					src="/assets/white_wordmark.png"
+					:alt="configStore.get('sitename')"
 				/>
-				<span v-else>{{ siteSettings.sitename }}</span>
+				<span v-else>{{ configStore.get("sitename") }}</span>
 			</router-link>
 		</div>
 
@@ -165,7 +158,7 @@ onMounted(async () => {
 			<span v-if="!loggedIn && !hideLoggedOut" class="grouped">
 				<a class="nav-item" @click="openModal('login')">Login</a>
 				<a
-					v-if="!siteSettings.registrationDisabled"
+					v-if="!configStore.get('registrationDisabled')"
 					class="nav-item"
 					@click="openModal('register')"
 					>Register</a
@@ -174,7 +167,7 @@ onMounted(async () => {
 		</div>
 
 		<christmas-lights
-			v-if="siteSettings.christmas"
+			v-if="configStore.get('christmas')"
 			:lights="Math.min(Math.max(Math.floor(windowWidth / 175), 5), 15)"
 		/>
 	</nav>

+ 4 - 2
frontend/src/components/Modal.spec.ts

@@ -1,6 +1,7 @@
 import { flushPromises } from "@vue/test-utils";
 import { h } from "vue";
 import { getWrapper } from "@/tests/utils/utils";
+import { useConfigStore } from "@/stores/config";
 import { useModalsStore } from "@/stores/modals";
 import Modal from "@/components/Modal.vue";
 
@@ -32,9 +33,10 @@ describe("Modal component", () => {
 	});
 
 	test("christmas lights render if enabled", async () => {
+		const configStore = useConfigStore();
+		configStore.config.christmas = true;
 		const wrapper = await getWrapper(Modal, {
-			shallow: true,
-			lofig: { siteSettings: { christmas: true } }
+			shallow: true
 		});
 		expect(
 			wrapper.findComponent({ name: "ChristmasLights" }).exists()

+ 8 - 8
frontend/src/components/Modal.vue

@@ -1,5 +1,6 @@
 <script setup lang="ts">
-import { defineAsyncComponent, ref, onMounted } from "vue";
+import { defineAsyncComponent } from "vue";
+import { useConfigStore } from "@/stores/config";
 import { useModalsStore } from "@/stores/modals";
 
 const ChristmasLights = defineAsyncComponent(
@@ -12,13 +13,8 @@ defineProps({
 	split: { type: Boolean, default: false }
 });
 
-const christmas = ref(false);
-
+const configStore = useConfigStore();
 const { closeCurrentModal } = useModalsStore();
-
-onMounted(async () => {
-	christmas.value = await lofig.get("siteSettings.christmas");
-});
 </script>
 
 <template>
@@ -42,7 +38,11 @@ onMounted(async () => {
 					>highlight_off</span
 				>
 				<Transition>
-					<christmas-lights v-if="christmas" small :lights="5" />
+					<christmas-lights
+						v-if="configStore.get('christmas')"
+						small
+						:lights="5"
+					/>
 				</Transition>
 			</header>
 			<section class="modal-card-body">

+ 5 - 8
frontend/src/components/PlaylistItem.vue

@@ -1,6 +1,7 @@
 <script setup lang="ts">
-import { defineAsyncComponent, ref, computed, onMounted } from "vue";
+import { defineAsyncComponent, computed } from "vue";
 import utils from "@/utils";
+import { useConfigStore } from "@/stores/config";
 
 const UserLink = defineAsyncComponent(
 	() => import("@/components/UserLink.vue")
@@ -11,7 +12,7 @@ const props = defineProps({
 	showOwner: { type: Boolean, default: false }
 });
 
-const sitename = ref("Musare");
+const configStore = useConfigStore();
 
 const totalLength = playlist => {
 	let length = 0;
@@ -27,10 +28,6 @@ const playlistLength = computed(
 			props.playlist.songs.length === 1 ? "song" : "songs"
 		}`
 );
-
-onMounted(async () => {
-	sitename.value = await lofig.get("siteSettings.sitename");
-});
 </script>
 
 <template>
@@ -53,8 +50,8 @@ onMounted(async () => {
 				<span v-if="showOwner"
 					><a
 						v-if="playlist.createdBy === 'Musare'"
-						:title="sitename"
-						>{{ sitename }}</a
+						:title="configStore.get('sitename')"
+						>{{ configStore.get("sitename") }}</a
 					><user-link v-else :user-id="playlist.createdBy" />
 				</span>

+ 4 - 2
frontend/src/components/ProfilePicture.vue

@@ -1,5 +1,6 @@
 <script setup lang="ts">
 import { ref, computed, onMounted } from "vue";
+import { useConfigStore } from "@/stores/config";
 
 const props = defineProps({
 	avatar: {
@@ -25,9 +26,10 @@ const initials = computed(() =>
 		.toUpperCase()
 );
 
+const configStore = useConfigStore();
+
 onMounted(async () => {
-	const frontendDomain = await lofig.get("frontendDomain");
-	notes.value = encodeURI(`${frontendDomain}/assets/notes.png`);
+	notes.value = encodeURI(`${configStore.urls.client}/assets/notes.png`);
 });
 </script>
 

+ 6 - 16
frontend/src/components/Request.vue

@@ -2,6 +2,7 @@
 import { defineAsyncComponent, ref, computed, onMounted, watch } from "vue";
 import Toast from "toasters";
 import { useWebsocketsStore } from "@/stores/websockets";
+import { useConfigStore } from "@/stores/config";
 import { useStationStore } from "@/stores/station";
 import { useManageStationStore } from "@/stores/manageStation";
 import { useSearchYoutube } from "@/composables/useSearchYoutube";
@@ -32,15 +33,14 @@ const { soundcloudDirect, addToQueue: soundcloudAddToQueue } =
 	useSoundcloudDirect();
 
 const { socket } = useWebsocketsStore();
+const configStore = useConfigStore();
 const stationStore = useStationStore();
 const manageStationStore = useManageStationStore({
 	modalUuid: props.modalUuid
 });
 
 const tab = ref("songs");
-const sitename = ref("Musare");
 const tabs = ref({});
-const experimentalDisableYoutubeSearch = ref(false);
 
 const station = computed({
 	get() {
@@ -133,18 +133,6 @@ watch(
 );
 
 onMounted(async () => {
-	sitename.value = await lofig.get("siteSettings.sitename");
-
-	lofig.get("experimental").then(experimental => {
-		if (
-			experimental &&
-			Object.hasOwn(experimental, "disable_youtube_search") &&
-			experimental.disable_youtube_search
-		) {
-			experimentalDisableYoutubeSearch.value = true;
-		}
-	});
-
 	showTab("songs");
 });
 </script>
@@ -206,7 +194,7 @@ onMounted(async () => {
 			<div class="tab" v-show="tab === 'songs'">
 				<div class="musare-songs">
 					<label class="label">
-						Search for a song on {{ sitename }}
+						Search for a song on {{ configStore.get("sitename") }}
 					</label>
 					<div class="control is-grouped input-with-button">
 						<p class="control is-expanded">
@@ -299,7 +287,9 @@ onMounted(async () => {
 
 				<div
 					class="youtube-search"
-					v-if="!experimentalDisableYoutubeSearch"
+					v-if="
+						!configStore.get('experimental.disable_youtube_search')
+					"
 				>
 					<label class="label"> Search for a song on YouTube </label>
 					<div class="control is-grouped input-with-button">

+ 7 - 20
frontend/src/components/modals/EditPlaylist/Tabs/AddSongs.vue

@@ -1,11 +1,12 @@
 <script setup lang="ts">
-import { defineAsyncComponent, ref, watch, onMounted } from "vue";
+import { defineAsyncComponent, watch } from "vue";
 import { storeToRefs } from "pinia";
 import { useSearchYoutube } from "@/composables/useSearchYoutube";
 import { useSearchMusare } from "@/composables/useSearchMusare";
 import { useYoutubeDirect } from "@/composables/useYoutubeDirect";
 import { useSoundcloudDirect } from "@/composables/useSoundcloudDirect";
 import { useSpotifyDirect } from "@/composables/useSpotifyDirect";
+import { useConfigStore } from "@/stores/config";
 import { useEditPlaylistStore } from "@/stores/editPlaylist";
 
 const SongItem = defineAsyncComponent(
@@ -19,12 +20,10 @@ const props = defineProps({
 	modalUuid: { type: String, required: true }
 });
 
+const configStore = useConfigStore();
 const editPlaylistStore = useEditPlaylistStore({ modalUuid: props.modalUuid });
 const { playlist } = storeToRefs(editPlaylistStore);
 
-const sitename = ref("Musare");
-const experimentalDisableYoutubeSearch = ref(false);
-
 const {
 	youtubeSearch,
 	searchForSongs,
@@ -114,26 +113,14 @@ watch(
 		);
 	}
 );
-
-onMounted(async () => {
-	sitename.value = await lofig.get("siteSettings.sitename");
-
-	lofig.get("experimental").then(experimental => {
-		if (
-			experimental &&
-			Object.hasOwn(experimental, "disable_youtube_search") &&
-			experimental.disable_youtube_search
-		) {
-			experimentalDisableYoutubeSearch.value = true;
-		}
-	});
-});
 </script>
 
 <template>
 	<div class="youtube-tab section">
 		<div>
-			<label class="label"> Search for a song on {{ sitename }}</label>
+			<label class="label">
+				Search for a song on {{ configStore.get("sitename") }}</label
+			>
 			<div class="control is-grouped input-with-button">
 				<p class="control is-expanded">
 					<input
@@ -220,7 +207,7 @@ onMounted(async () => {
 			</p>
 		</div>
 
-		<div v-if="!experimentalDisableYoutubeSearch">
+		<div v-if="!configStore.get('experimental.disable_youtube_search')">
 			<label class="label"> Search for a song from YouTube </label>
 			<div class="control is-grouped input-with-button">
 				<p class="control is-expanded">

+ 3 - 5
frontend/src/components/modals/EditPlaylist/index.vue

@@ -10,6 +10,7 @@ import Toast from "toasters";
 import { storeToRefs } from "pinia";
 import { DraggableList } from "vue-draggable-list";
 import { useWebsocketsStore } from "@/stores/websockets";
+import { useConfigStore } from "@/stores/config";
 import { useEditPlaylistStore } from "@/stores/editPlaylist";
 import { useStationStore } from "@/stores/station";
 import { useUserAuthStore } from "@/stores/userAuth";
@@ -35,6 +36,7 @@ const props = defineProps({
 });
 
 const { socket } = useWebsocketsStore();
+const configStore = useConfigStore();
 const editPlaylistStore = useEditPlaylistStore({ modalUuid: props.modalUuid });
 const stationStore = useStationStore();
 const userAuthStore = useUserAuthStore();
@@ -43,7 +45,6 @@ const { station } = storeToRefs(stationStore);
 const { loggedIn, userId, role: userRole } = storeToRefs(userAuthStore);
 
 const drag = ref(false);
-const apiDomain = ref("");
 const gettingSongs = ref(false);
 const tabs = ref([]);
 const songItems = ref([]);
@@ -188,10 +189,7 @@ const removePlaylist = () => {
 };
 
 const downloadPlaylist = async () => {
-	if (apiDomain.value === "")
-		apiDomain.value = await lofig.get("backend.apiDomain");
-
-	fetch(`${apiDomain.value}/export/playlist/${playlist.value._id}`, {
+	fetch(`${configStore.urls.api}/export/playlist/${playlist.value._id}`, {
 		credentials: "include"
 	})
 		.then(res => res.blob())

+ 6 - 6
frontend/src/components/modals/EditSong/Tabs/Songs.vue

@@ -1,6 +1,7 @@
 <script setup lang="ts">
-import { defineAsyncComponent, ref, onMounted } from "vue";
+import { defineAsyncComponent, onMounted } from "vue";
 
+import { useConfigStore } from "@/stores/config";
 import { useEditSongStore } from "@/stores/editSong";
 
 import { useSearchMusare } from "@/composables/useSearchMusare";
@@ -14,8 +15,7 @@ const props = defineProps({
 	modalModulePath: { type: String, default: "modals/editSong/MODAL_UUID" }
 });
 
-const sitename = ref("Musare");
-
+const configStore = useConfigStore();
 const { form } = useEditSongStore({ modalUuid: props.modalUuid });
 
 const {
@@ -26,8 +26,6 @@ const {
 } = useSearchMusare();
 
 onMounted(async () => {
-	sitename.value = await lofig.get("siteSettings.sitename");
-
 	musareSearch.value.query = form.inputs.title.value;
 	searchForMusareSongs(1, false);
 });
@@ -35,7 +33,9 @@ onMounted(async () => {
 
 <template>
 	<div class="musare-songs-tab">
-		<label class="label"> Search for a song on {{ sitename }} </label>
+		<label class="label">
+			Search for a song on {{ configStore.get("sitename") }}
+		</label>
 		<div class="control is-grouped input-with-button">
 			<p class="control is-expanded">
 				<input

+ 3 - 16
frontend/src/components/modals/EditSong/Tabs/Youtube.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
 import { storeToRefs } from "pinia";
-import { onMounted, ref } from "vue";
 
+import { useConfigStore } from "@/stores/config";
 import { useEditSongStore } from "@/stores/editSong";
 
 import { useSearchYoutube } from "@/composables/useSearchYoutube";
@@ -14,6 +14,7 @@ const props = defineProps({
 	modalModulePath: { type: String, default: "modals/editSong/MODAL_UUID" }
 });
 
+const configStore = useConfigStore();
 const editSongStore = useEditSongStore({ modalUuid: props.modalUuid });
 
 const { form, newSong } = storeToRefs(editSongStore);
@@ -23,8 +24,6 @@ const { updateYoutubeId } = editSongStore;
 const { youtubeSearch, searchForSongs, loadMoreSongs } = useSearchYoutube();
 const { youtubeDirect, getYoutubeVideoId } = useYoutubeDirect();
 
-const experimentalDisableYoutubeSearch = ref(false);
-
 const selectSong = (youtubeId, result = null) => {
 	updateYoutubeId(youtubeId);
 
@@ -34,18 +33,6 @@ const selectSong = (youtubeId, result = null) => {
 			thumbnail: result.thumbnail
 		});
 };
-
-onMounted(() => {
-	lofig.get("experimental").then(experimental => {
-		if (
-			experimental &&
-			Object.hasOwn(experimental, "disable_youtube_search") &&
-			experimental.disable_youtube_search
-		) {
-			experimentalDisableYoutubeSearch.value = true;
-		}
-	});
-});
 </script>
 
 <template>
@@ -70,7 +57,7 @@ onMounted(() => {
 			</p>
 		</div>
 
-		<div v-if="!experimentalDisableYoutubeSearch">
+		<div v-if="!configStore.get('experimental.disable_youtube_search')">
 			<label class="label"> Search for a song from YouTube </label>
 			<div class="control is-grouped input-with-button">
 				<p class="control is-expanded">

+ 5 - 5
frontend/src/components/modals/EditSong/index.vue

@@ -18,6 +18,7 @@ import { useSoundcloudPlayer } from "@/composables/useSoundcloudPlayer";
 import { Song } from "@/types/song.js";
 
 import { useWebsocketsStore } from "@/stores/websockets";
+import { useConfigStore } from "@/stores/config";
 import { useModalsStore } from "@/stores/modals";
 import { useEditSongStore } from "@/stores/editSong";
 import { useStationStore } from "@/stores/station";
@@ -102,7 +103,6 @@ const youtubeErrorMessage = ref("");
 const youtubeVideoDuration = ref("0.000");
 const youtubeVideoCurrentTime = ref<number | string>(0);
 const youtubeVideoNote = ref("");
-const useHTTPS = ref(false);
 const muted = ref(false);
 const volumeSliderValue = ref(0);
 const activityWatchMediaDataInterval = ref(null);
@@ -196,6 +196,8 @@ const currentSongFlagged = computed(
 );
 // EditSongs end
 
+const configStore = useConfigStore();
+
 const {
 	editSong,
 	stopVideo,
@@ -323,10 +325,10 @@ const { inputs, unsavedChanges, save, setValue, setOriginalValue } = useForm(
 			validate: value => {
 				if (!validation.isLength(value, 8, 256))
 					return "Thumbnail must have between 8 and 256 characters.";
-				if (useHTTPS.value && value.indexOf("https://") !== 0)
+				if (configStore.urls.secure && value.indexOf("https://") !== 0)
 					return 'Thumbnail must start with "https://".';
 				if (
-					!useHTTPS.value &&
+					!configStore.urls.secure &&
 					value.indexOf("https://") !== 0 &&
 					value.indexOf("http://") !== 0
 				)
@@ -1219,8 +1221,6 @@ onMounted(async () => {
 		sendActivityWatchMediaData();
 	}, 1000);
 
-	useHTTPS.value = await lofig.get("cookie.secure");
-
 	socket.onConnect(() => {
 		if (newSong.value && !mediaSource.value && !bulk.value) {
 			setSong({

+ 6 - 14
frontend/src/components/modals/Login.vue

@@ -1,7 +1,8 @@
 <script setup lang="ts">
-import { defineAsyncComponent, ref, onMounted } from "vue";
+import { defineAsyncComponent, ref } from "vue";
 import { useRoute } from "vue-router";
 import Toast from "toasters";
+import { useConfigStore } from "@/stores/config";
 import { useUserAuthStore } from "@/stores/userAuth";
 import { useModalsStore } from "@/stores/modals";
 
@@ -14,13 +15,9 @@ const password = ref({
 	value: "",
 	visible: false
 });
-const apiDomain = ref("");
-const siteSettings = ref({
-	registrationDisabled: false,
-	githubAuthentication: false
-});
 const passwordElement = ref();
 
+const configStore = useConfigStore();
 const { login } = useUserAuthStore();
 
 const { openModal, closeCurrentModal } = useModalsStore();
@@ -65,11 +62,6 @@ const changeToRegisterModal = () => {
 const githubRedirect = () => {
 	localStorage.setItem("github_redirect", route.path);
 };
-
-onMounted(async () => {
-	apiDomain.value = await lofig.get("backend.apiDomain");
-	siteSettings.value = await lofig.get("siteSettings");
-});
 </script>
 
 <template>
@@ -149,9 +141,9 @@ onMounted(async () => {
 						Login
 					</button>
 					<a
-						v-if="siteSettings.githubAuthentication"
+						v-if="configStore.get('githubAuthentication')"
 						class="button is-github"
-						:href="apiDomain + '/auth/github/authorize'"
+						:href="configStore.urls.api + '/auth/github/authorize'"
 						@click="githubRedirect()"
 					>
 						<div class="icon">
@@ -165,7 +157,7 @@ onMounted(async () => {
 				</div>
 
 				<p
-					v-if="!siteSettings.registrationDisabled"
+					v-if="!configStore.get('registrationDisabled')"
 					class="content-box-optional-helper"
 				>
 					<a @click="changeToRegisterModal()">

+ 1 - 1
frontend/src/components/modals/ManageStation/index.vue

@@ -642,7 +642,7 @@ onBeforeUnmount(() => {
 .manage-station-modal.modal .modal-card {
 	.tab > button {
 		width: 100%;
-		margin-bottom: 10px;
+		margin-top: 10px;
 	}
 	.currently-playing.song-item {
 		height: 130px;

+ 31 - 46
frontend/src/components/modals/Register.vue

@@ -2,6 +2,7 @@
 import { defineAsyncComponent, ref, watch, onMounted } from "vue";
 import { useRoute } from "vue-router";
 import Toast from "toasters";
+import { useConfigStore } from "@/stores/config";
 import { useUserAuthStore } from "@/stores/userAuth";
 import { useModalsStore } from "@/stores/modals";
 import validation from "@/validation";
@@ -33,20 +34,12 @@ const password = ref({
 	message:
 		"Include at least one lowercase letter, one uppercase letter, one number and one special character."
 });
-const recaptcha = ref({
-	key: "",
-	token: "",
-	enabled: false
-});
-const apiDomain = ref("");
-const siteSettings = ref({
-	registrationDisabled: false,
-	githubAuthentication: false
-});
+const recaptchaToken = ref("");
 const passwordElement = ref();
 
 const { register } = useUserAuthStore();
 
+const configStore = useConfigStore();
 const { openModal, closeCurrentModal } = useModalsStore();
 
 const submitModal = () => {
@@ -57,7 +50,7 @@ const submitModal = () => {
 		username: username.value.value,
 		email: email.value.value,
 		password: password.value.value,
-		recaptchaToken: recaptcha.value.token
+		recaptchaToken: recaptchaToken.value
 	})
 		.then((res: any) => {
 			if (res.status === "success") window.location.reload();
@@ -146,39 +139,31 @@ watch(
 );
 
 onMounted(async () => {
-	apiDomain.value = await lofig.get("backend.apiDomain");
-	lofig.get("siteSettings").then(settings => {
-		if (settings.registrationDisabled) {
-			new Toast("Registration is disabled.");
-			closeCurrentModal();
-		} else {
-			siteSettings.value = settings;
-		}
-	});
-
-	lofig.get("recaptcha").then(obj => {
-		recaptcha.value.enabled = obj.enabled;
-		if (obj.enabled === true) {
-			recaptcha.value.key = obj.key;
-
-			const recaptchaScript = document.createElement("script");
-			recaptchaScript.onload = () => {
-				grecaptcha.ready(() => {
-					grecaptcha
-						.execute(recaptcha.value.key, { action: "login" })
-						.then(token => {
-							recaptcha.value.token = token;
-						});
-				});
-			};
-
-			recaptchaScript.setAttribute(
-				"src",
-				`https://www.google.com/recaptcha/api.js?render=${recaptcha.value.key}`
-			);
-			document.head.appendChild(recaptchaScript);
-		}
-	});
+	if (configStore.get("registrationDisabled")) {
+		new Toast("Registration is disabled.");
+		closeCurrentModal();
+	}
+
+	if (configStore.get("recaptcha.enabled")) {
+		const recaptchaScript = document.createElement("script");
+		recaptchaScript.onload = () => {
+			grecaptcha.ready(() => {
+				grecaptcha
+					.execute(configStore.get("recaptcha.key"), {
+						action: "login"
+					})
+					.then(token => {
+						recaptchaToken.value = token;
+					});
+			});
+		};
+
+		recaptchaScript.setAttribute(
+			"src",
+			`https://www.google.com/recaptcha/api.js?render=${recaptcha.value.key}`
+		);
+		document.head.appendChild(recaptchaScript);
+	}
 });
 </script>
 
@@ -282,9 +267,9 @@ onMounted(async () => {
 						Register
 					</button>
 					<a
-						v-if="siteSettings.githubAuthentication"
+						v-if="configStore.get('githubAuthentication')"
 						class="button is-github"
-						:href="apiDomain + '/auth/github/authorize'"
+						:href="configStore.urls.api + '/auth/github/authorize'"
 						@click="githubRedirect()"
 					>
 						<div class="icon">

+ 18 - 21
frontend/src/components/modals/RemoveAccount.vue

@@ -2,6 +2,7 @@
 import { defineAsyncComponent, ref, onMounted } from "vue";
 import { useRoute } from "vue-router";
 import Toast from "toasters";
+import { useConfigStore } from "@/stores/config";
 import { useSettingsStore } from "@/stores/settings";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useModalsStore } from "@/stores/modals";
@@ -16,6 +17,7 @@ const props = defineProps({
 	githubLinkConfirmed: { type: Boolean, default: false }
 });
 
+const configStore = useConfigStore();
 const settingsStore = useSettingsStore();
 const route = useRoute();
 
@@ -26,14 +28,11 @@ const { isPasswordLinked, isGithubLinked } = settingsStore;
 const { closeCurrentModal } = useModalsStore();
 
 const step = ref("confirm-identity");
-const apiDomain = ref("");
-const accountRemovalMessage = ref("");
 const password = ref({
 	value: "",
 	visible: false
 });
 const passwordElement = ref();
-const githubAuthentication = ref(false);
 
 const checkForAutofill = (cb, event) => {
 	if (
@@ -91,25 +90,19 @@ const relinkGithub = () => {
 const remove = () =>
 	socket.dispatch("users.remove", res => {
 		if (res.status === "success") {
-			return socket.dispatch("users.logout", () =>
-				lofig.get("cookie").then(cookie => {
-					document.cookie = `${cookie.SIDname}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
-					closeCurrentModal();
-					window.location.href = "/";
-				})
-			);
+			return socket.dispatch("users.logout", () => {
+				document.cookie = `${configStore.get(
+					"cookie"
+				)}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
+				closeCurrentModal();
+				window.location.href = "/";
+			});
 		}
 
 		return new Toast(res.message);
 	});
 
 onMounted(async () => {
-	apiDomain.value = await lofig.get("backend.apiDomain");
-	accountRemovalMessage.value = await lofig.get("messages.accountRemoval");
-	githubAuthentication.value = await lofig.get(
-		"siteSettings.githubAuthentication"
-	);
-
 	if (props.githubLinkConfirmed === true) confirmGithubLink();
 });
 </script>
@@ -164,7 +157,8 @@ onMounted(async () => {
 				id="password-linked"
 				v-if="
 					step === 'confirm-identity' &&
-					(isPasswordLinked || !githubAuthentication)
+					(isPasswordLinked ||
+						!configStore.get('githubAuthentication'))
 				"
 			>
 				<h2 class="content-box-title">Enter your password</h2>
@@ -223,7 +217,7 @@ onMounted(async () => {
 			<div
 				class="content-box"
 				v-else-if="
-					githubAuthentication &&
+					configStore.get('githubAuthentication') &&
 					isGithubLinked &&
 					step === 'confirm-identity'
 				"
@@ -248,7 +242,10 @@ onMounted(async () => {
 
 			<div
 				class="content-box"
-				v-if="githubAuthentication && step === 'relink-github'"
+				v-if="
+					configStore.get('githubAuthentication') &&
+					step === 'relink-github'
+				"
 			>
 				<h2 class="content-box-title">Re-link GitHub</h2>
 				<p class="content-box-description">
@@ -260,7 +257,7 @@ onMounted(async () => {
 					<a
 						class="button is-github"
 						@click="relinkGithub()"
-						:href="`${apiDomain}/auth/github/link`"
+						:href="`${configStore.urls.api}/auth/github/link`"
 					>
 						<div class="icon">
 							<img
@@ -284,7 +281,7 @@ onMounted(async () => {
 			>
 				<h2 class="content-box-title">Remove your account</h2>
 				<p class="content-box-description">
-					{{ accountRemovalMessage }}
+					{{ configStore.get("messages.accountRemoval") }}
 				</p>
 
 				<div class="content-box-inputs">

+ 6 - 7
frontend/src/keyboardShortcuts.ts

@@ -1,3 +1,5 @@
+import { useConfigStore } from "@/stores/config";
+
 const shortcuts = {};
 
 let _shortcuts = [];
@@ -32,13 +34,10 @@ const lib = {
 		shortcuts[name].shift = shortcuts[name].shift
 			? shortcuts[name].shift
 			: false;
-		lofig.get("shortcutOverrides").then(overrides => {
-			if (overrides && overrides[name])
-				shortcuts[name] = Object.assign(
-					shortcuts[name],
-					overrides[name]
-				);
-		});
+		const configStore = useConfigStore();
+		const overrides = configStore.get("shortcutOverrides");
+		if (overrides && overrides[name])
+			shortcuts[name] = Object.assign(shortcuts[name], overrides[name]);
 		lib.remakeShortcutsArray();
 	},
 

+ 6 - 40
frontend/src/main.ts

@@ -4,9 +4,9 @@ import { createApp } from "vue";
 import VueTippy, { Tippy } from "vue-tippy";
 import { createRouter, createWebHistory } from "vue-router";
 import { createPinia } from "pinia";
-import "lofig";
 import Toast from "toasters";
 
+import { useConfigStore } from "@/stores/config";
 import { useUserAuthStore } from "@/stores/userAuth";
 import { useUserPreferencesStore } from "@/stores/userPreferences";
 import { useModalsStore } from "@/stores/modals";
@@ -16,23 +16,9 @@ import i18n from "@/i18n";
 
 import AppComponent from "./App.vue";
 
-const defaultConfigURL = new URL(
-	"/config/default.json",
-	import.meta.url
-).toString();
-
-const REQUIRED_CONFIG_VERSION = 13;
-
-lofig.folder = defaultConfigURL;
-
 const handleMetadata = attrs => {
-	lofig.get("siteSettings.sitename").then(siteName => {
-		if (siteName) {
-			document.title = `${siteName} | ${attrs.title}`;
-		} else {
-			document.title = `Musare | ${attrs.title}`;
-		}
-	});
+	const configStore = useConfigStore();
+	document.title = `${configStore.get("sitename")} | ${attrs.title}`;
 };
 
 const app = createApp(AppComponent);
@@ -271,6 +257,7 @@ app.use(createPinia());
 
 const { createSocket } = useWebsocketsStore();
 createSocket().then(async socket => {
+	const configStore = useConfigStore();
 	const userAuthStore = useUserAuthStore();
 	const modalsStore = useModalsStore();
 
@@ -350,22 +337,8 @@ createSocket().then(async socket => {
 
 	app.use(router);
 
-	lofig.fetchConfig().then(config => {
-		const { configVersion, skipConfigVersionCheck } = config;
-		if (
-			configVersion !== REQUIRED_CONFIG_VERSION &&
-			!skipConfigVersionCheck
-		) {
-			// eslint-disable-next-line no-alert
-			alert(
-				"CONFIG VERSION IS WRONG. PLEASE UPDATE YOUR CONFIG WITH THE HELP OF THE TEMPLATE FILE AND THE README FILE."
-			);
-			window.stop();
-		}
-	});
-
 	socket.on("ready", res => {
-		const { loggedIn, role, username, userId, email } = res.data;
+		const { loggedIn, role, username, userId, email } = res.user;
 
 		userAuthStore.authData({
 			loggedIn,
@@ -430,14 +403,7 @@ createSocket().then(async socket => {
 		});
 	});
 
-	lofig.get("experimental").then(experimental => {
-		if (
-			experimental &&
-			Object.hasOwn(experimental, "media_session") &&
-			experimental.media_session
-		)
-			ms.init();
-	});
+	if (configStore.get("experimental.media_session")) ms.init();
 
 	app.mount("#root");
 });

+ 0 - 6
frontend/src/pages/Admin/index.vue

@@ -29,10 +29,6 @@ const { socket } = useWebsocketsStore();
 const { hasPermission } = useUserAuthStore();
 
 const currentTab = ref("");
-const siteSettings = ref({
-	logo: "",
-	sitename: ""
-});
 const sidebarActive = ref(true);
 const sidebarPadding = ref(0);
 const keyboardShortcutsHelper = ref();
@@ -119,8 +115,6 @@ onMounted(async () => {
 		router.push(`/admin/songs`);
 	}
 
-	siteSettings.value = await lofig.get("siteSettings");
-
 	sidebarActive.value = JSON.parse(
 		localStorage.getItem("admin-sidebar-active")
 	);

+ 17 - 16
frontend/src/pages/Home.vue

@@ -13,6 +13,7 @@ import { storeToRefs } from "pinia";
 import { DraggableList } from "vue-draggable-list";
 import { useI18n } from "vue-i18n";
 import { useWebsocketsStore } from "@/stores/websockets";
+import { useConfigStore } from "@/stores/config";
 import { useUserAuthStore } from "@/stores/userAuth";
 import { useModalsStore } from "@/stores/modals";
 import keyboardShortcuts from "@/keyboardShortcuts";
@@ -32,6 +33,7 @@ const UserLink = defineAsyncComponent(
 
 const { t } = useI18n();
 
+const configStore = useConfigStore();
 const userAuthStore = useUserAuthStore();
 const route = useRoute();
 const router = useRouter();
@@ -43,11 +45,6 @@ const { socket } = useWebsocketsStore();
 
 const stations = ref([]);
 const searchQuery = ref("");
-const siteSettings = ref({
-	logo_white: "",
-	sitename: "Musare",
-	registrationDisabled: false
-});
 const orderOfFavoriteStations = ref([]);
 const handledLoginRegisterRedirect = ref(false);
 
@@ -168,8 +165,6 @@ watch(
 );
 
 onMounted(async () => {
-	siteSettings.value = await lofig.get("siteSettings");
-
 	if (route.query.searchQuery)
 		searchQuery.value = JSON.stringify(route.query.query);
 
@@ -381,13 +376,13 @@ onBeforeUnmount(() => {
 				<div class="content-container">
 					<div class="content">
 						<img
-							v-if="siteSettings.sitename === 'Musare'"
-							:src="siteSettings.logo_white"
-							:alt="siteSettings.sitename || `Musare`"
+							v-if="configStore.get('sitename') === 'Musare'"
+							src="/assets/white_wordmark.png"
+							:alt="configStore.get('sitename')"
 							class="logo"
 						/>
 						<span v-else class="logo">{{
-							siteSettings.sitename
+							configStore.get("sitename")
 						}}</span>
 						<div v-if="!loggedIn" class="buttons">
 							<button
@@ -397,7 +392,7 @@ onBeforeUnmount(() => {
 								{{ t("Login") }}
 							</button>
 							<button
-								v-if="!siteSettings.registrationDisabled"
+								v-if="!configStore.get('registrationDisabled')"
 								class="button register"
 								@click="openModal('register')"
 							>
@@ -535,10 +530,14 @@ onBeforeUnmount(() => {
 														'official'
 													"
 													:title="
-														siteSettings.sitename
+														configStore.get(
+															'sitename'
+														)
 													"
 													>{{
-														siteSettings.sitename
+														configStore.get(
+															"sitename"
+														)
 													}}</span
 												>
 												<user-link
@@ -806,8 +805,10 @@ onBeforeUnmount(() => {
 									<span class="host">
 										<span
 											v-if="station.type === 'official'"
-											:title="siteSettings.sitename"
-											>{{ siteSettings.sitename }}</span
+											:title="configStore.get('sitename')"
+											>{{
+												configStore.get("sitename")
+											}}</span
 										>
 										<user-link
 											v-else

+ 13 - 15
frontend/src/pages/Settings/Tabs/Security.vue

@@ -1,7 +1,8 @@
 <script setup lang="ts">
-import { defineAsyncComponent, ref, watch, reactive, onMounted } from "vue";
+import { defineAsyncComponent, ref, watch, reactive } from "vue";
 import Toast from "toasters";
 import { storeToRefs } from "pinia";
+import { useConfigStore } from "@/stores/config";
 import { useSettingsStore } from "@/stores/settings";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useUserAuthStore } from "@/stores/userAuth";
@@ -14,16 +15,12 @@ const QuickConfirm = defineAsyncComponent(
 	() => import("@/components/QuickConfirm.vue")
 );
 
+const configStore = useConfigStore();
 const settingsStore = useSettingsStore();
 const userAuthStore = useUserAuthStore();
 
 const { socket } = useWebsocketsStore();
 
-const apiDomain = ref("");
-const siteSettings = ref({
-	sitename: "Musare",
-	githubAuthentication: false
-});
 const validation = reactive({
 	oldPassword: {
 		value: "",
@@ -98,11 +95,6 @@ const removeSessions = () => {
 	});
 };
 
-onMounted(async () => {
-	apiDomain.value = await lofig.get("backend.apiDomain");
-	siteSettings.value = await lofig.get("siteSettings");
-});
-
 watch(validation, newValidation => {
 	const { value } = newValidation.newPassword;
 	if (!_validation.isLength(value, 6, 200)) {
@@ -220,15 +212,18 @@ watch(validation, newValidation => {
 			<div class="section-margin-bottom" />
 		</div>
 
-		<div v-if="!isGithubLinked && siteSettings.githubAuthentication">
+		<div v-if="!isGithubLinked && configStore.get('githubAuthentication')">
 			<h4 class="section-title">Link your GitHub account</h4>
 			<p class="section-description">
-				Link your {{ siteSettings.sitename }} account with GitHub
+				Link your {{ configStore.get("sitename") }} account with GitHub
 			</p>
 
 			<hr class="section-horizontal-rule" />
 
-			<a class="button is-github" :href="`${apiDomain}/auth/github/link`">
+			<a
+				class="button is-github"
+				:href="`${configStore.urls.api}/auth/github/link`"
+			>
 				<div class="icon">
 					<img class="invert" src="/assets/social/github.svg" />
 				</div>
@@ -248,7 +243,10 @@ watch(validation, newValidation => {
 
 			<div class="row">
 				<quick-confirm
-					v-if="isPasswordLinked && siteSettings.githubAuthentication"
+					v-if="
+						isPasswordLinked &&
+						configStore.get('githubAuthentication')
+					"
 					@confirm="unlinkPassword()"
 				>
 					<a class="button is-danger">

+ 5 - 5
frontend/src/pages/Station/Sidebar/Users.vue

@@ -11,17 +11,18 @@ import {
 import Toast from "toasters";
 import { storeToRefs } from "pinia";
 import { useWebsocketsStore } from "@/stores/websockets";
+import { useConfigStore } from "@/stores/config";
 import { useStationStore } from "@/stores/station";
 
 const ProfilePicture = defineAsyncComponent(
 	() => import("@/components/ProfilePicture.vue")
 );
 
+const configStore = useConfigStore();
 const stationStore = useStationStore();
 const route = useRoute();
 
 const notesUri = ref("");
-const frontendDomain = ref("");
 const tab = ref("active");
 const tabs = ref([]);
 const search = reactive({
@@ -64,7 +65,7 @@ const { hasPermission } = stationStore;
 const copyToClipboard = async () => {
 	try {
 		await navigator.clipboard.writeText(
-			frontendDomain.value + route.fullPath
+			configStore.urls.client + route.fullPath
 		);
 	} catch (err) {
 		new Toast("Failed to copy to clipboard.");
@@ -127,9 +128,8 @@ watch(
 	}
 );
 
-onMounted(async () => {
-	frontendDomain.value = await lofig.get("frontendDomain");
-	notesUri.value = encodeURI(`${frontendDomain.value}/assets/notes.png`);
+onMounted(() => {
+	notesUri.value = encodeURI(`${configStore.urls.client}/assets/notes.png`);
 });
 </script>
 

+ 15 - 22
frontend/src/pages/Station/index.vue

@@ -14,6 +14,7 @@ import { ContentLoader } from "vue-content-loader";
 import canAutoPlay from "can-autoplay";
 import { useSoundcloudPlayer } from "@/composables/useSoundcloudPlayer";
 import { useWebsocketsStore } from "@/stores/websockets";
+import { useConfigStore } from "@/stores/config";
 import { useStationStore } from "@/stores/station";
 import { useUserAuthStore } from "@/stores/userAuth";
 import { useUserPreferencesStore } from "@/stores/userPreferences";
@@ -52,6 +53,7 @@ const route = useRoute();
 const router = useRouter();
 
 const { socket } = useWebsocketsStore();
+const configStore = useConfigStore();
 const stationStore = useStationStore();
 const userAuthStore = useUserAuthStore();
 const userPreferencesStore = useUserPreferencesStore();
@@ -104,12 +106,9 @@ const beforeMediaModalLocalPausedLock = ref(false);
 const beforeMediaModalLocalPaused = ref(null);
 const persistentToastCheckerInterval = ref(null);
 const persistentToasts = ref([]);
-const christmas = ref(false);
-const sitename = ref("Musare");
 // Experimental options
 const experimentalChangableListenModeEnabled = ref(false);
 const experimentalChangableListenMode = ref("listen_and_participate"); // Can be either listen_and_participate or participate
-const experimentalMediaSession = ref(false);
 // End experimental options
 // NEW
 const videoLoading = ref();
@@ -674,7 +673,9 @@ const youtubeReady = () => {
 					if (isApple.value) {
 						updateLocalPaused(true);
 						new Toast(
-							`Please click play manually to use ${sitename.value} on iOS.`
+							`Please click play manually to use ${configStore.get(
+								"sitename"
+							)} on iOS.`
 						);
 					}
 				},
@@ -847,7 +848,8 @@ const setCurrentSong = data => {
 
 	clearTimeout(window.stationNextSongTimeout);
 
-	if (experimentalMediaSession.value) updateMediaSessionData(_currentSong);
+	if (configStore.get("experimental.media_session"))
+		updateMediaSessionData(_currentSong);
 
 	startedAt.value = _startedAt;
 	updateStationPaused(_paused);
@@ -976,7 +978,7 @@ const changeVolume = () => {
 	changePlayerVolume();
 };
 const resumeLocalPlayer = () => {
-	if (experimentalMediaSession.value)
+	if (configStore.get("experimental.media_session"))
 		updateMediaSessionData(currentSong.value);
 	if (!noSong.value) {
 		playerSeekTo(getTimeElapsed() / 1000 + currentSong.value.skipDuration);
@@ -984,7 +986,7 @@ const resumeLocalPlayer = () => {
 	}
 };
 const pauseLocalPlayer = () => {
-	if (experimentalMediaSession.value)
+	if (configStore.get("experimental.media_session"))
 		updateMediaSessionData(currentSong.value);
 	if (!noSong.value) {
 		timeBeforePause.value = getTimeElapsed();
@@ -1225,7 +1227,7 @@ onMounted(async () => {
 		);
 	}, 1000);
 
-	const experimental = await lofig.get("experimental");
+	const experimental = configStore.get("experimental");
 
 	socket.onConnect(() => {
 		console.debug(TAG, "On socked connect start");
@@ -1778,17 +1780,7 @@ onMounted(async () => {
 		});
 	});
 
-	frontendDevMode.value = await lofig.get("mode");
-	christmas.value = await lofig.get("siteSettings.christmas");
-	sitename.value = await lofig.get("siteSettings.sitename");
-	lofig.get("experimental").then(experimental => {
-		if (
-			experimental &&
-			Object.hasOwn(experimental, "media_session") &&
-			experimental.media_session
-		)
-			experimentalMediaSession.value = true;
-	});
+	frontendDevMode.value = process.env.NODE_ENV;
 
 	ms.setListeners(0, {
 		play: () => {
@@ -1879,7 +1871,7 @@ onMounted(async () => {
 onBeforeUnmount(() => {
 	document.getElementsByTagName("html")[0].style.cssText = "";
 
-	if (experimentalMediaSession.value) {
+	if (configStore.get("experimental.media_session")) {
 		ms.removeListeners(0);
 		ms.removeMediaSessionData(0);
 	}
@@ -2235,7 +2227,8 @@ onBeforeUnmount(() => {
 								<div
 									id="seeker-bar"
 									:class="{
-										'christmas-seeker': christmas,
+										'christmas-seeker':
+											configStore.get('christmas'),
 										nyan:
 											currentSong &&
 											currentSong.mediaSource ===
@@ -2324,7 +2317,7 @@ onBeforeUnmount(() => {
 								/>
 								<img
 									v-if="
-										christmas &&
+										configStore.get('christmas') &&
 										currentSong &&
 										![
 											'youtube:QH2-TGUlwu4',

+ 70 - 0
frontend/src/stores/config.ts

@@ -0,0 +1,70 @@
+import { defineStore } from "pinia";
+
+export const useConfigStore = defineStore("config", {
+	state: (): {
+		config: {
+			cookie: string;
+			sitename: string;
+			recaptcha: {
+				enabled: boolean;
+				key: string;
+			};
+			githubAuthentication: boolean;
+			messages: Record<string, string>;
+			christmas: boolean;
+			footerLinks: Record<string, string | boolean>;
+			shortcutOverrides: Record<string, any>;
+			registrationDisabled: boolean;
+			experimental: {
+				changable_listen_mode: string[];
+				media_session: boolean;
+				disable_youtube_search: boolean;
+			};
+		};
+	} => ({
+		config: {
+			cookie: "musareSID",
+			sitename: "Musare",
+			recaptcha: {
+				enabled: false,
+				key: ""
+			},
+			githubAuthentication: false,
+			messages: {
+				accountRemoval:
+					"Your account will be deactivated instantly and your data will shortly be deleted by an admin."
+			},
+			christmas: false,
+			footerLinks: {},
+			shortcutOverrides: {},
+			registrationDisabled: false,
+			experimental: {
+				changable_listen_mode: [],
+				media_session: false,
+				disable_youtube_search: false
+			}
+		}
+	}),
+	actions: {
+		setConfig(config) {
+			this.config = config;
+		},
+		get(query: string) {
+			let { config } = this;
+			query.split(".").forEach(property => {
+				config = config[property];
+			});
+			return config;
+		}
+	},
+	getters: {
+		urls() {
+			const { protocol, host } = document.location;
+			const secure = protocol !== "http:";
+			const client = `${protocol}//${host}`;
+			const api = `${client}/backend`;
+			const ws = `${secure ? "wss" : "ws"}://${host}/backend/ws`;
+			return { client, api, ws, host, secure };
+		}
+	}
+});

+ 6 - 6
frontend/src/stores/modals.ts

@@ -3,6 +3,7 @@ import { defineStore } from "pinia";
 import utils from "@/utils";
 
 import { useWebsocketsStore } from "@/stores/websockets";
+import { useConfigStore } from "@/stores/config";
 
 export const useModalsStore = defineStore("modals", {
 	state: (): {
@@ -35,12 +36,11 @@ export const useModalsStore = defineStore("modals", {
 		closeModal(uuid: string, force = false) {
 			Object.entries(this.modals).forEach(([_uuid, modal]) => {
 				if (uuid === _uuid) {
-					if (modal.modal === "register")
-						lofig
-							.get("recaptcha.enabled")
-							.then((enabled: boolean) => {
-								if (enabled) window.location.reload();
-							});
+					if (modal.modal === "register") {
+						const configStore = useConfigStore();
+						if (configStore.get("recaptcha.enabled"))
+							window.location.reload();
+					}
 					const close = () => {
 						const { socket } = useWebsocketsStore();
 						socket.destroyModalListeners(uuid);

+ 49 - 47
frontend/src/stores/userAuth.ts

@@ -2,6 +2,7 @@ import { defineStore } from "pinia";
 import Toast from "toasters";
 import validation from "@/validation";
 import { useWebsocketsStore } from "@/stores/websockets";
+import { useConfigStore } from "@/stores/config";
 
 export const useUserAuthStore = defineStore("userAuth", {
 	state: (): {
@@ -97,6 +98,7 @@ export const useUserAuthStore = defineStore("userAuth", {
 					);
 				else {
 					const { socket } = useWebsocketsStore();
+					const configStore = useConfigStore();
 					socket.dispatch(
 						"users.register",
 						username,
@@ -106,29 +108,29 @@ export const useUserAuthStore = defineStore("userAuth", {
 						res => {
 							if (res.status === "success") {
 								if (res.SID) {
-									return lofig.get("cookie").then(cookie => {
-										const date = new Date();
-										date.setTime(
-											new Date().getTime() +
-												2 * 365 * 24 * 60 * 60 * 1000
-										);
+									const date = new Date();
+									date.setTime(
+										new Date().getTime() +
+											2 * 365 * 24 * 60 * 60 * 1000
+									);
 
-										const secure = cookie.secure
-											? "secure=true; "
-											: "";
+									const secure = configStore.urls.secure
+										? "secure=true; "
+										: "";
 
-										let domain = "";
-										if (cookie.domain !== "localhost")
-											domain = ` domain=${cookie.domain};`;
+									let domain = "";
+									if (configStore.urls.host !== "localhost")
+										domain = ` domain=${configStore.urls.host};`;
 
-										document.cookie = `${cookie.SIDname}=${
-											res.SID
-										}; expires=${date.toUTCString()}; ${domain}${secure}path=/`;
+									document.cookie = `${configStore.get(
+										"cookie"
+									)}=${
+										res.SID
+									}; expires=${date.toUTCString()}; ${domain}${secure}path=/`;
 
-										return resolve({
-											status: "success",
-											message: "Account registered!"
-										});
+									return resolve({
+										status: "success",
+										message: "Account registered!"
 									});
 								}
 
@@ -146,35 +148,35 @@ export const useUserAuthStore = defineStore("userAuth", {
 				const { email, password } = user;
 
 				const { socket } = useWebsocketsStore();
+				const configStore = useConfigStore();
 				socket.dispatch("users.login", email, password, res => {
 					if (res.status === "success") {
-						return lofig.get("cookie").then(cookie => {
-							const date = new Date();
-							date.setTime(
-								new Date().getTime() +
-									2 * 365 * 24 * 60 * 60 * 1000
-							);
+						const date = new Date();
+						date.setTime(
+							new Date().getTime() + 2 * 365 * 24 * 60 * 60 * 1000
+						);
 
-							const secure = cookie.secure ? "secure=true; " : "";
+						const secure = configStore.urls.secure
+							? "secure=true; "
+							: "";
 
-							let domain = "";
-							if (cookie.domain !== "localhost")
-								domain = ` domain=${cookie.domain};`;
+						let domain = "";
+						if (configStore.urls.host !== "localhost")
+							domain = ` domain=${configStore.urls.host};`;
 
-							document.cookie = `${cookie.SIDname}=${
-								res.data.SID
-							}; expires=${date.toUTCString()}; ${domain}${secure}path=/`;
+						document.cookie = `${configStore.get("cookie")}=${
+							res.data.SID
+						}; expires=${date.toUTCString()}; ${domain}${secure}path=/`;
 
-							const bc = new BroadcastChannel(
-								`${cookie.SIDname}.user_login`
-							);
-							bc.postMessage(true);
-							bc.close();
+						const bc = new BroadcastChannel(
+							`${configStore.get("cookie")}.user_login`
+						);
+						bc.postMessage(true);
+						bc.close();
 
-							return resolve({
-								status: "success",
-								message: "Logged in!"
-							});
+						return resolve({
+							status: "success",
+							message: "Logged in!"
 						});
 					}
 
@@ -187,12 +189,12 @@ export const useUserAuthStore = defineStore("userAuth", {
 				const { socket } = useWebsocketsStore();
 				socket.dispatch("users.logout", res => {
 					if (res.status === "success") {
-						return resolve(
-							lofig.get("cookie").then(cookie => {
-								document.cookie = `${cookie.SIDname}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
-								return window.location.reload();
-							})
-						);
+						const configStore = useConfigStore();
+						document.cookie = `${configStore.get(
+							"cookie"
+						)}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
+						window.location.reload();
+						return resolve(true);
 					}
 					new Toast(res.message);
 					return reject(new Error(res.message));

+ 18 - 20
frontend/src/stores/websockets.ts

@@ -12,29 +12,27 @@ export const useWebsocketsStore = defineStore("websockets", {
 	actions: {
 		createSocket(): Promise<SocketHandler> {
 			return new Promise(resolve => {
-				lofig.get("backend.websocketsDomain").then(websocketsDomain => {
-					const { listeners } = this.socket.dispatcher;
+				const { listeners } = this.socket.dispatcher;
 
-					this.socket = new SocketHandler(websocketsDomain);
+				this.socket = new SocketHandler();
 
-					// only executes if the websocket object is being replaced
-					if (listeners) {
-						// for each listener type
-						Object.keys(listeners).forEach(listenerType =>
-							// for each callback previously present for the listener type
-							listeners[listenerType].forEach(element => {
-								// add the listener back after the websocket object is reset
-								this.socket.dispatcher.addEventListener(
-									listenerType,
-									element.cb,
-									element.options
-								);
-							})
-						);
-					}
+				// only executes if the websocket object is being replaced
+				if (listeners) {
+					// for each listener type
+					Object.keys(listeners).forEach(listenerType =>
+						// for each callback previously present for the listener type
+						listeners[listenerType].forEach(element => {
+							// add the listener back after the websocket object is reset
+							this.socket.dispatcher.addEventListener(
+								listenerType,
+								element.cb,
+								element.options
+							);
+						})
+					);
+				}
 
-					resolve(this.socket);
-				});
+				resolve(this.socket);
 			});
 		}
 	},

+ 0 - 1
frontend/src/tests/utils/setup.ts

@@ -1,4 +1,3 @@
-import "lofig";
 import SocketHandlerMock from "@/classes/__mocks__/SocketHandler.class";
 
 vi.clearAllMocks();

+ 0 - 16
frontend/src/tests/utils/utils.ts

@@ -4,12 +4,6 @@ import { flushPromises, mount } from "@vue/test-utils";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useUserAuthStore } from "@/stores/userAuth";
 
-let config;
-const getConfig = async () => {
-	if (!config) config = await import("../../../dist/config/template.json");
-	return config;
-};
-
 export const getWrapper = async (
 	component: any,
 	options?: {
@@ -22,7 +16,6 @@ export const getWrapper = async (
 			stubActions?: boolean;
 		};
 		mockSocket?: boolean | { data?: any; executeDispatch?: boolean };
-		lofig?: any;
 		loginRequired?: boolean;
 		baseTemplate?: string;
 		attachTo?: HTMLElement | null;
@@ -77,15 +70,6 @@ export const getWrapper = async (
 		};
 	else opts.global.components = components;
 
-	await getConfig();
-	if (opts.lofig) {
-		lofig.config = {
-			...config,
-			...opts.lofig
-		};
-		delete opts.lofig;
-	} else lofig.config = config;
-
 	if (opts.mockSocket) {
 		const websocketsStore = useWebsocketsStore();
 		await websocketsStore.createSocket();

+ 0 - 5
frontend/src/types/global.d.ts

@@ -2,11 +2,6 @@
 /* eslint vars-on-top: 0 */
 
 declare global {
-	var lofig: {
-		config: object;
-		fetchConfig: () => Promise<any>;
-		get: (setting: string) => any;
-	};
 	var stationInterval: number;
 	var YT: any;
 	var stationNextSongTimeout: any;

+ 15 - 21
frontend/vite.config.js

@@ -2,7 +2,6 @@ import path from "path";
 import vue from "@vitejs/plugin-vue";
 import dynamicImport from "vite-plugin-dynamic-import";
 import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite";
-import config from "config";
 import fs from "fs";
 
 const fetchVersionAndGitInfo = () => {
@@ -23,7 +22,7 @@ const fetchVersionAndGitInfo = () => {
 		);
 
 		console.log(`Musare version: ${packageJson.version}.`);
-		if (config.has("debug.version") && config.get("debug.version"))
+		if (process.env.MUSARE_DEBUG_VERSION)
 			debug.version = packageJson.version;
 	} catch (e) {
 		console.log(`Could not get package info: ${e.message}.`);
@@ -78,13 +77,13 @@ const fetchVersionAndGitInfo = () => {
 			console.log(
 				`Git branch: ${remote}/${branch}. Remote url: ${remoteUrl}. Latest commit: ${latestCommit} (${latestCommitShort}).`
 			);
-			if (config.get("debug.git.remote")) debug.git.remote = remote;
-			if (config.get("debug.git.remoteUrl"))
+			if (process.env.MUSARE_DEBUG_GIT_REMOTE) debug.git.remote = remote;
+			if (process.env.MUSARE_DEBUG_GIT_REMOTE_URL)
 				debug.git.remoteUrl = remoteUrl;
-			if (config.get("debug.git.branch")) debug.git.branch = branch;
-			if (config.get("debug.git.latestCommit"))
+			if (process.env.MUSARE_DEBUG_GIT_BRANCH) debug.git.branch = branch;
+			if (process.env.MUSARE_DEBUG_GIT_LATEST_COMMIT)
 				debug.git.latestCommit = latestCommit;
-			if (config.get("debug.git.latestCommitShort"))
+			if (process.env.MUSARE_DEBUG_GIT_LATEST_COMMIT_SHORT)
 				debug.git.latestCommitShort = latestCommitShort;
 		}
 	} catch (e) {
@@ -96,16 +95,15 @@ const fetchVersionAndGitInfo = () => {
 
 const debug = fetchVersionAndGitInfo();
 
-const siteName = config.has("siteSettings.sitename")
-	? config.get("siteSettings.sitename")
-	: "Musare";
-
 const htmlPlugin = () => ({
 	name: "html-transform",
 	transformIndexHtml(originalHtml) {
 		let html = originalHtml;
 
-		html = html.replace(/{{ title }}/g, siteName);
+		html = html.replace(
+			/{{ title }}/g,
+			process.env.MUSARE_SITENAME ?? "Musare"
+		);
 		html = html.replace(/{{ version }}/g, debug.version);
 		html = html.replace(/{{ gitRemote }}/g, debug.git.remote);
 		html = html.replace(/{{ gitRemoteUrl }}/g, debug.git.remoteUrl);
@@ -120,24 +118,20 @@ const htmlPlugin = () => ({
 	}
 });
 
-const mode = process.env.FRONTEND_MODE || "dev";
-
 let server = null;
 
-if (mode === "dev")
+if (process.env.FRONTEND_MODE === "development")
 	server = {
 		host: "0.0.0.0",
-		port: config.has("devServer.port") ? config.get("devServer.port") : 81,
+		port: process.env.FRONTEND_DEV_PORT ?? 81,
 		strictPort: true,
 		hmr: {
-			clientPort: config.has("devServer.hmrClientPort")
-				? config.get("devServer.hmrClientPort")
-				: 80
+			clientPort: process.env.FRONTEND_CLIENT_PORT ?? 80
 		}
 	};
 
 export default {
-	mode: mode === "dev" ? "development" : "production",
+	mode: process.env.FRONTEND_MODE,
 	root: "src",
 	publicDir: "../dist",
 	base: "/",
@@ -164,7 +158,7 @@ export default {
 		]
 	},
 	define: {
-		__VUE_PROD_DEVTOOLS__: config.get("mode") === "development",
+		__VUE_PROD_DEVTOOLS__: !!process.env.FRONTEND_PROD_DEVTOOLS,
 		MUSARE_VERSION: JSON.stringify(debug.version),
 		MUSARE_GIT_REMOTE: JSON.stringify(debug.git.remote),
 		MUSARE_GIT_REMOTE_URL: JSON.stringify(debug.git.remoteUrl),

+ 3 - 7
musare.sh

@@ -51,7 +51,7 @@ if [[ ${dockerInstalled} -gt 0 || ${composeInstalled} -gt 0 ]]; then
 fi
 
 composeFiles="-f docker-compose.yml"
-if [[ ${CONTAINER_MODE} == "dev" ]]; then
+if [[ ${CONTAINER_MODE} == "development" ]]; then
     composeFiles="${composeFiles} -f docker-compose.dev.yml"
 fi
 if [[ -f docker-compose.override.yml ]]; then
@@ -425,9 +425,8 @@ case $1 in
         fi
         musareshChange=$(echo "${updated}" | grep "musare.sh")
         dbChange=$(echo "${updated}" | grep "backend/logic/db/schemas")
-        fcChange=$(echo "${updated}" | grep "frontend/dist/config/template.json")
-        bcChange=$(echo "${updated}" | grep "backend/config/template.json")
-        if [[ ( $2 == "auto" && -z $dbChange && -z $fcChange && -z $bcChange && -z $musareshChange ) || -z $2 ]]; then
+        bcChange=$(echo "${updated}" | grep "backend/config/default.json")
+        if [[ ( $2 == "auto" && -z $dbChange && -z $bcChange && -z $musareshChange ) || -z $2 ]]; then
             if [[ -n $musareshChange && $(git diff @\{u\} -- musare.sh) != "" ]]; then
                 if [[ $musareshModified != "" ]]; then
                     echo -e "${RED}musare.sh has been modified, please reset these changes and run the update command again to continue.${NC}"
@@ -450,9 +449,6 @@ case $1 in
                 if [[ -n $dbChange ]]; then
                     echo -e "${RED}Database schema has changed, please run migration!${NC}"
                 fi
-                if [[ -n $fcChange ]]; then
-                    echo -e "${RED}Frontend config has changed, please update!${NC}"
-                fi
                 if [[ -n $bcChange ]]; then
                     echo -e "${RED}Backend config has changed, please update!${NC}"
                 fi

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.