Browse Source

feat: Installs feathers client

Owen Diffey 3 months ago
parent
commit
e5928b6b7c

+ 1 - 1
Dockerfile

@@ -43,7 +43,7 @@ COPY --from=server_node_modules --link /opt/app/package.json /opt/app/package-lo
 
 ENTRYPOINT npm run migrate && npm run start
 
-EXPOSE 8080
+EXPOSE 3030
 
 # Backend node modules
 FROM common_base AS backend_node_modules

+ 1 - 1
Dockerfile.dev

@@ -34,7 +34,7 @@ COPY --chown=musare:musare --link --from=server_node_modules /opt/app/node_modul
 
 ENTRYPOINT sh /opt/app/entrypoint.dev.sh
 
-EXPOSE 8080
+EXPOSE 3030
 
 # Backend node modules
 FROM common_base AS backend_node_modules

File diff suppressed because it is too large
+ 921 - 12
frontend/package-lock.json


+ 5 - 0
frontend/package.json

@@ -41,6 +41,8 @@
     "vue-tsc": "^2.1.10"
   },
   "dependencies": {
+    "@feathersjs/feathers": "^5.0.31",
+    "@feathersjs/socketio-client": "^5.0.31",
     "@intlify/unplugin-vue-i18n": "^5.3.0",
     "@vitejs/plugin-vue": "^5.1.4",
     "can-autoplay": "^3.0.2",
@@ -48,9 +50,12 @@
     "date-fns": "^4.1.0",
     "dompurify": "^3.1.7",
     "eslint-config-airbnb-base": "^15.0.0",
+    "feathers-pinia": "^4.5.4",
     "marked": "^15.0.0",
+    "musare-server": "http://server:3030/server-0.0.0.tgz",
     "normalize.css": "^8.0.1",
     "pinia": "^2.2.6",
+    "socket.io-client": "^4.8.1",
     "toasters": "^2.3.1",
     "typescript": "^5.6.3",
     "vite": "^5.4.10",

+ 3 - 1
frontend/src/App.vue

@@ -13,6 +13,7 @@ import { useUserPreferencesStore } from "@/stores/userPreferences";
 import { useModalsStore } from "@/stores/modals";
 import aw from "@/aw";
 import keyboardShortcuts from "@/keyboardShortcuts";
+import { useAuthStore } from "@/stores/auth";
 
 const ModalManager = defineAsyncComponent(
 	() => import("@/components/ModalManager.vue")
@@ -32,6 +33,7 @@ const configStore = useConfigStore();
 const userAuthStore = useUserAuthStore();
 const userPreferencesStore = useUserPreferencesStore();
 const modalsStore = useModalsStore();
+const authStore = useAuthStore();
 
 const socketConnected = ref(true);
 const keyIsDown = ref("");
@@ -274,7 +276,7 @@ onMounted(async () => {
 <template>
 	<div class="upper-container">
 		<banned-page v-if="banned" />
-		<div v-else class="upper-container">
+		<div v-else-if="authStore.isInitDone" class="upper-container">
 			<router-view :key="$route.fullPath" class="main-container" />
 		</div>
 		<falling-snow v-if="christmas" />

+ 3 - 0
frontend/src/classes/SocketHandler.class.ts

@@ -71,6 +71,7 @@ export default class SocketHandler {
 	}
 
 	init() {
+		return;
 		const configStore = useConfigStore();
 		const userAuthStore = useUserAuthStore();
 
@@ -151,6 +152,7 @@ export default class SocketHandler {
 			modalUuid?: string;
 		}
 	) {
+		return;
 		this.dispatcher.addEventListener(
 			target,
 			(event: CustomEvent) => cb(...event.detail),
@@ -159,6 +161,7 @@ export default class SocketHandler {
 	}
 
 	dispatch(...args: [string, ...any[]]) {
+		return;
 		if (!this.socket || this.socket.readyState !== 1) {
 			this.pendingDispatches.push(() => this.dispatch(...args));
 			return undefined;

+ 12 - 10
frontend/src/components/MainHeader.vue

@@ -7,6 +7,7 @@ import { useConfigStore } from "@/stores/config";
 import { useUserAuthStore } from "@/stores/userAuth";
 import { useUserPreferencesStore } from "@/stores/userPreferences";
 import { useModalsStore } from "@/stores/modals";
+import { useAuthStore } from "@/stores/auth";
 
 const ChristmasLights = defineAsyncComponent(
 	() => import("@/components/ChristmasLights.vue")
@@ -31,8 +32,9 @@ const configStore = useConfigStore();
 const { cookie, sitename, registrationDisabled, christmas } =
 	storeToRefs(configStore);
 
-const { loggedIn, username } = storeToRefs(userAuthStore);
-const { logout, hasPermission } = userAuthStore;
+const authStore = useAuthStore();
+
+const { hasPermission } = userAuthStore;
 const userPreferencesStore = useUserPreferencesStore();
 const { nightmode } = storeToRefs(userPreferencesStore);
 
@@ -42,7 +44,7 @@ const toggleNightmode = toggle => {
 	localNightmode.value =
 		toggle === undefined ? !localNightmode.value : toggle;
 
-	if (loggedIn.value) {
+	if (authStore.isAuthenticated) {
 		socket.dispatch(
 			"users.updatePreferences",
 			{ nightmode: localNightmode.value },
@@ -77,7 +79,7 @@ onMounted(async () => {
 <template>
 	<nav
 		class="nav is-info"
-		:class="{ transparent, 'hide-logged-out': !loggedIn && hideLoggedOut }"
+		:class="{ transparent, 'hide-logged-out': !authStore.isAuthenticated && hideLoggedOut }"
 	>
 		<div class="nav-left">
 			<router-link v-if="!hideLogo" class="nav-item is-brand" to="/">
@@ -91,7 +93,7 @@ onMounted(async () => {
 		</div>
 
 		<span
-			v-if="loggedIn || !hideLoggedOut"
+			v-if="authStore.isAuthenticated || !hideLoggedOut"
 			class="nav-toggle"
 			:class="{ 'is-active': isMobile }"
 			tabindex="0"
@@ -124,7 +126,7 @@ onMounted(async () => {
 				</span>
 				<span class="night-mode-label">Toggle Nightmode</span>
 			</div>
-			<span v-if="loggedIn" class="grouped">
+			<span v-if="authStore.isAuthenticated" class="grouped">
 				<router-link
 					v-if="hasPermission('admin.view')"
 					class="nav-item admin"
@@ -136,7 +138,7 @@ onMounted(async () => {
 					class="nav-item"
 					:to="{
 						name: 'profile',
-						params: { username },
+						params: { username: authStore.user.username },
 						query: { tab: 'playlists' }
 					}"
 				>
@@ -146,7 +148,7 @@ onMounted(async () => {
 					class="nav-item"
 					:to="{
 						name: 'profile',
-						params: { username }
+						params: { username: authStore.user.username }
 					}"
 				>
 					Profile
@@ -154,9 +156,9 @@ onMounted(async () => {
 				<router-link class="nav-item" to="/settings"
 					>Settings</router-link
 				>
-				<a class="nav-item" @click="logout()">Logout</a>
+				<a class="nav-item" @click="authStore.logout()">Logout</a>
 			</span>
-			<span v-if="!loggedIn && !hideLoggedOut" class="grouped">
+			<span v-if="!authStore.isAuthenticated && !hideLoggedOut" class="grouped">
 				<a class="nav-item" @click="openModal('login')">Login</a>
 				<a
 					v-if="!registrationDisabled"

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

@@ -6,6 +6,7 @@ import { storeToRefs } from "pinia";
 import { useConfigStore } from "@/stores/config";
 import { useUserAuthStore } from "@/stores/userAuth";
 import { useModalsStore } from "@/stores/modals";
+import { useAuthStore } from "@/stores/auth";
 
 const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
 
@@ -20,19 +21,20 @@ const passwordElement = ref();
 
 const configStore = useConfigStore();
 const { githubAuthentication, registrationDisabled } = storeToRefs(configStore);
-const { login } = useUserAuthStore();
+const authStore = useAuthStore();
 
 const { openModal, closeCurrentModal } = useModalsStore();
 
 const submitModal = () => {
 	if (!email.value || !password.value.value) return;
 
-	login({
+	authStore.authenticate({
+		strategy: "local",
 		email: email.value,
 		password: password.value.value
 	})
-		.then((res: any) => {
-			if (res.status === "success") window.location.reload();
+		.then(() => {
+			window.location.reload();
 		})
 		.catch(err => new Toast(err.message));
 };

+ 33 - 0
frontend/src/feathers.ts

@@ -0,0 +1,33 @@
+import { createClient } from "musare-server"
+import socketio from "@feathersjs/socketio-client"
+import io from "socket.io-client"
+import { createPiniaClient } from "feathers-pinia"
+import { pinia } from "./pinia";
+import { useAuthStore } from "./stores/auth";
+
+const socket = io(`${document.location.protocol}//${document.location.host}`, {
+    path: "/api/socket.io",
+    transports: ["websocket"]
+})
+
+const feathersClient = createClient(socketio(socket), { storage: window.localStorage })
+
+export const api = createPiniaClient(feathersClient, {
+    pinia,
+    idField: '_id',
+    // optional
+    ssr: false,
+    whitelist: [],
+    paramsForServer: [],
+    skipGetIfExists: true,
+    customSiftOperators: {},
+    setupInstance(data) {
+      return data
+    },
+    customizeStore(defaultStore) {
+      return {}
+    },
+    services: {},
+});
+
+console.log(111, feathersClient, api);

+ 25 - 2
frontend/src/main.ts

@@ -3,7 +3,6 @@ import { createApp } from "vue";
 
 import VueTippy, { Tippy } from "vue-tippy";
 import { createRouter, createWebHistory } from "vue-router";
-import { createPinia } from "pinia";
 import Toast from "toasters";
 
 import { useConfigStore } from "@/stores/config";
@@ -16,6 +15,10 @@ import i18n from "@/i18n";
 
 import AppComponent from "./App.vue";
 
+import { pinia } from "./pinia";
+import { useAuthStore } from "./stores/auth";
+import { api } from "./feathers";
+
 const handleMetadata = attrs => {
 	const configStore = useConfigStore();
 	document.title = `${configStore.sitename} | ${attrs.title}`;
@@ -257,7 +260,27 @@ const router = createRouter({
 	]
 });
 
-app.use(createPinia());
+app.use(pinia);
+
+// console.log(222, await api.service('users').create({
+// 	username: 'test',
+// 	email: 'test@test.com',
+// 	password: 'password'
+// }));
+
+// const authStore = useAuthStore();
+// authStore.authenticate({
+// 	strategy: 'local',
+// 	email: 'test@test.com',
+// 	password: 'password'
+// })
+// authStore.authenticate()
+// 	.then(async () => {
+// 		const users = await api.service('users').find();
+
+// 		console.log(333, users);
+// 	});
+
 
 const { createSocket } = useWebsocketsStore();
 createSocket().then(async socket => {

+ 15 - 14
frontend/src/pages/Home.vue

@@ -17,6 +17,7 @@ import { useConfigStore } from "@/stores/config";
 import { useUserAuthStore } from "@/stores/userAuth";
 import { useModalsStore } from "@/stores/modals";
 import keyboardShortcuts from "@/keyboardShortcuts";
+import { useAuthStore } from "@/stores/auth";
 
 const MainHeader = defineAsyncComponent(
 	() => import("@/components/MainHeader.vue")
@@ -34,12 +35,12 @@ const UserLink = defineAsyncComponent(
 const { t } = useI18n();
 
 const configStore = useConfigStore();
+const authStore = useAuthStore();
 const userAuthStore = useUserAuthStore();
 const route = useRoute();
 const router = useRouter();
 
 const { sitename, registrationDisabled } = storeToRefs(configStore);
-const { loggedIn, userId } = storeToRefs(userAuthStore);
 const { hasPermission } = userAuthStore;
 
 const { socket } = useWebsocketsStore();
@@ -49,9 +50,9 @@ const searchQuery = ref("");
 const orderOfFavoriteStations = ref([]);
 const handledLoginRegisterRedirect = ref(false);
 
-const isOwner = station => loggedIn.value && station.owner === userId.value;
+const isOwner = station => authStore.isAuthenticated && station.owner === authStore.userId;
 const isDj = station =>
-	loggedIn.value && !!station.djs.find(dj => dj === userId.value);
+	authStore.isAuthenticated && !!station.djs.find(dj => dj === authStore.userId);
 const isOwnerOrDj = station => isOwner(station) || isDj(station);
 
 const isPlaying = station => typeof station.currentSong.title !== "undefined";
@@ -120,7 +121,7 @@ const fetchStations = () => {
 
 const canRequest = (station, requireLogin = true) =>
 	station &&
-	(!requireLogin || loggedIn.value) &&
+	(!requireLogin || authStore.isAuthenticated) &&
 	station.requests &&
 	station.requests.enabled &&
 	(station.requests.access === "user" ||
@@ -170,7 +171,7 @@ onMounted(async () => {
 		searchQuery.value = JSON.stringify(route.query.query);
 
 	if (
-		!loggedIn.value &&
+		!authStore.isAuthenticated &&
 		route.redirectedFrom &&
 		(route.redirectedFrom.name === "login" ||
 			route.redirectedFrom.name === "register") &&
@@ -315,11 +316,11 @@ onMounted(async () => {
 	});
 
 	socket.on("event:station.djs.added", res => {
-		if (res.data.user._id === userId.value) fetchStations();
+		if (res.data.user._id === authStore.userId) fetchStations();
 	});
 
 	socket.on("event:station.djs.removed", res => {
-		if (res.data.user._id === userId.value) fetchStations();
+		if (res.data.user._id === authStore.userId) fetchStations();
 	});
 
 	socket.on("keep.event:user.role.updated", () => {
@@ -371,7 +372,7 @@ onBeforeUnmount(() => {
 				:transparent="true"
 				:hide-logged-out="true"
 			/>
-			<div class="header" :class="{ loggedIn }">
+			<div class="header" :class="{ loggedIn: authStore.isAuthenticated }">
 				<img class="background" src="/assets/homebg.jpeg" />
 				<div class="overlay"></div>
 				<div class="content-container">
@@ -383,7 +384,7 @@ onBeforeUnmount(() => {
 							class="logo"
 						/>
 						<span v-else class="logo">{{ sitename }}</span>
-						<div v-if="!loggedIn" class="buttons">
+						<div v-if="!authStore.isAuthenticated" class="buttons">
 							<button
 								class="button login"
 								@click="openModal('login')"
@@ -482,7 +483,7 @@ onBeforeUnmount(() => {
 									<div class="displayName">
 										<i
 											v-if="
-												loggedIn && !element.isFavorited
+												authStore.isAuthenticated && !element.isFavorited
 											"
 											@click.prevent="
 												favoriteStation(element._id)
@@ -494,7 +495,7 @@ onBeforeUnmount(() => {
 										>
 										<i
 											v-if="
-												loggedIn && element.isFavorited
+												authStore.isAuthenticated && element.isFavorited
 											"
 											@click.prevent="
 												unfavoriteStation(element._id)
@@ -646,7 +647,7 @@ onBeforeUnmount(() => {
 					</div>
 				</div>
 				<a
-					v-if="loggedIn"
+					v-if="authStore.isAuthenticated"
 					@click="openModal('createStation')"
 					class="station-card createStation"
 				>
@@ -758,7 +759,7 @@ onBeforeUnmount(() => {
 						<div class="media">
 							<div class="displayName">
 								<i
-									v-if="loggedIn && !station.isFavorited"
+									v-if="authStore.isAuthenticated && !station.isFavorited"
 									@click.prevent="
 										favoriteStation(station._id)
 									"
@@ -768,7 +769,7 @@ onBeforeUnmount(() => {
 									>{{ t("Icons.Favorite") }}</i
 								>
 								<i
-									v-if="loggedIn && station.isFavorited"
+									v-if="authStore.isAuthenticated && station.isFavorited"
 									@click.prevent="
 										unfavoriteStation(station._id)
 									"

+ 3 - 0
frontend/src/pinia.ts

@@ -0,0 +1,3 @@
+import { createPinia } from "pinia";
+
+export const pinia = createPinia();

+ 12 - 0
frontend/src/stores/auth.ts

@@ -0,0 +1,12 @@
+import { acceptHMRUpdate, defineStore } from 'pinia'
+import { api } from '@/feathers'
+import { useAuth } from 'feathers-pinia'
+
+export const useAuthStore = defineStore('auth', () => {
+  const auth = useAuth({ api, servicePath: 'users' });
+  auth.reAuthenticate();
+  return auth;
+})
+
+if (import.meta.hot)
+  import.meta.hot.accept(acceptHMRUpdate(useAuthStore, import.meta.hot))

+ 5 - 5
server/migrations/20241117173947_user.ts

@@ -3,13 +3,13 @@ import type { Knex } from 'knex'
 
 export async function up(knex: Knex): Promise<void> {
   await knex.schema.createTable('users', (table) => {
-    table.increments('id')
-
-    table.string('email').unique()
-    table.string('password')
+    table.increments('id');
+    table.string('username').unique();
+    table.string('email').unique();
+    table.string('password');
   })
 }
 
 export async function down(knex: Knex): Promise<void> {
-  await knex.schema.dropTable('users')
+  await knex.schema.dropTable('users');
 }

+ 3 - 2
server/src/services/users/users.schema.ts

@@ -12,6 +12,7 @@ import type { UserService } from './users.class'
 export const userSchema = Type.Object(
   {
     id: Type.Number(),
+    username: Type.String(),
     email: Type.String(),
     password: Type.Optional(Type.String())
   },
@@ -27,7 +28,7 @@ export const userExternalResolver = resolve<User, HookContext<UserService>>({
 })
 
 // Schema for creating new entries
-export const userDataSchema = Type.Pick(userSchema, ['email', 'password'], {
+export const userDataSchema = Type.Pick(userSchema, ['username', 'email', 'password'], {
   $id: 'UserData'
 })
 export type UserData = Static<typeof userDataSchema>
@@ -47,7 +48,7 @@ export const userPatchResolver = resolve<User, HookContext<UserService>>({
 })
 
 // Schema for allowed query properties
-export const userQueryProperties = Type.Pick(userSchema, ['id', 'email'])
+export const userQueryProperties = Type.Pick(userSchema, ['id','username', 'email'])
 export const userQuerySchema = Type.Intersect(
   [
     querySyntax(userQueryProperties),

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