From eac315bea1a2075858648bbc49c200b8020e1ff7 Mon Sep 17 00:00:00 2001 From: Janis Date: Wed, 10 Dec 2025 11:44:33 +0100 Subject: [PATCH] feat: FRO-2 Implement Login Component (#8) Reviewed-on: https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Frontend/pulls/8 Reviewed-by: lq64 Co-authored-by: Janis Co-committed-by: Janis --- public/env.js | 4 +++ public/env.template.js | 2 +- src/composables/useUserInfo.ts | 19 +++++++++++ src/main.ts | 15 ++++++++- src/router/index.ts | 53 ++++++++++++++++++------------ src/stores/auth.ts | 60 ---------------------------------- src/types/GameTypes.ts | 2 ++ src/types/authTypes.ts | 16 --------- src/views/LoginView.vue | 14 +++----- 9 files changed, 77 insertions(+), 108 deletions(-) create mode 100644 public/env.js create mode 100644 src/composables/useUserInfo.ts delete mode 100644 src/stores/auth.ts delete mode 100644 src/types/authTypes.ts diff --git a/public/env.js b/public/env.js new file mode 100644 index 0000000..fc45354 --- /dev/null +++ b/public/env.js @@ -0,0 +1,4 @@ +window.__RUNTIME_CONFIG__ = { + API_URL: "http://localhost:9000" +}; + diff --git a/public/env.template.js b/public/env.template.js index 9da9e66..cba0fc7 100644 --- a/public/env.template.js +++ b/public/env.template.js @@ -1,4 +1,4 @@ -globalThis.__RUNTIME_CONFIG__ = { +window.__RUNTIME_CONFIG__ = { API_URL: "${API_URL}" }; diff --git a/src/composables/useUserInfo.ts b/src/composables/useUserInfo.ts new file mode 100644 index 0000000..220796d --- /dev/null +++ b/src/composables/useUserInfo.ts @@ -0,0 +1,19 @@ +import { defineStore } from 'pinia' +import {ref, type Ref} from 'vue' + +export const useUserInfo = defineStore('userInfo', () => { + const username: Ref = ref(null); + const userId: Ref = ref(null); + + function setUserInfo(name: string, id: number) { + username.value = name; + userId.value = id; + } + + function clearUserInfo() { + username.value = null; + userId.value = null; + } + + return { username, userId, setUserInfo, clearUserInfo }; +}); diff --git a/src/main.ts b/src/main.ts index 73cdb29..13dd503 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,17 +11,18 @@ import 'quasar/dist/quasar.css' import { createPinia } from 'pinia' import axios from 'axios' import VueAxios from 'vue-axios' +import {useUserInfo} from "@/composables/useUserInfo.ts"; const app = createApp(App) const pinia = createPinia() +app.use(pinia) app.use(router) app.use(Quasar, { plugins: { Notify }, }) -app.use(pinia) app.use(VueAxios, axios) app.use(Particles, { init: async engine => { @@ -29,4 +30,16 @@ app.use(Particles, { }, }) +axios.interceptors.response.use( + res => res, + err => { + if (err.response?.status === 401) { + const info = useUserInfo(); + info.clearUserInfo(); + router.replace({name: 'login'}); + } + return Promise.reject(err); + } +); + app.mount('#app') diff --git a/src/router/index.ts b/src/router/index.ts index 08070ad..0c2ed87 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,49 +1,60 @@ import { createRouter, createWebHistory } from 'vue-router' -import HomeView from '../views/HomeView.vue' import LoginView from '../views/LoginView.vue' import MainMenuView from '../views/MainMenuView.vue' import createGameView from '../views/CreateGame.vue' import joinGameView from "@/views/JoinGameView.vue"; +import axios from "axios"; +import { useUserInfo } from "@/composables/useUserInfo"; + +const api = window?.__RUNTIME_CONFIG__?.API_URL; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', - name: 'home', - component: HomeView, + name: 'mainmenu', + component: MainMenuView, + meta: { requiresAuth: true } }, { path: '/login', name: 'login', component: LoginView, - }, - { - path: '/about', - name: 'about', - // route level code-splitting - // this generates a separate chunk (About.[hash].js) for this route - // which is lazy-loaded when the route is visited. - component: () => import('../views/AboutView.vue'), - }, - { - path: '/mainmenu', - name: 'mainmenu', - component: MainMenuView + meta: { requiresAuth: false } }, { path: '/create', name: 'create-Game', - component: createGameView + component: createGameView, + meta: { requiresAuth: true } }, { path: '/join', name: 'join-Game', - component: joinGameView - }, - - + component: joinGameView, + meta: { requiresAuth: true } + } ], }) +router.beforeEach(async (to, from, next) => { + const info = useUserInfo(); + if (!to.meta.requiresAuth) return next(); + try { + await axios.get(`${api}/userInfo`, { withCredentials: true }).then( + res => { + info.setUserInfo(res.data.username, res.data.userId); + } + ); + next(); + } catch (err) { + info.clearUserInfo(); + next('/login'); + } +}); + export default router + + + diff --git a/src/stores/auth.ts b/src/stores/auth.ts deleted file mode 100644 index 8d83040..0000000 --- a/src/stores/auth.ts +++ /dev/null @@ -1,60 +0,0 @@ -import {type credentials, type token, type user} from '@/types/authTypes' -import { defineStore } from 'pinia' -import {ref, computed, type Ref} from 'vue' - -export const useAuthStore = defineStore('auth', () => { - const user: Ref = ref(null) - const token = ref(localStorage.getItem('token') || null) - - const isAuthenticated = computed(() => !!token.value) - - async function login(credentials: credentials) { - const response = await fakeLoginApi(credentials) - - token.value = response.token - user.value = response.user - - localStorage.setItem('token', token.value) - } - - function setToken(newToken: string) { - token.value = newToken - localStorage.setItem('token', newToken) - } - - function logout() { - token.value = null - user.value = null - localStorage.removeItem('token') - } - - async function fetchUser() { - if (!token.value) return - - const response = await fakeFetchUserApi(token.value) - user.value = response.user - } - - return { - user, - token, - isAuthenticated, - login, - setToken, - logout, - fetchUser, - } -}) - -async function fakeLoginApi(credentials: credentials): Promise { - return { - token: 'abc123', - user: { id: 1, name: 'John Doe' } - } -} - -async function fakeFetchUserApi(token: string): Promise<{ user: user }> { - return { - user: { id: 1, name: 'John Doe' } - } -} diff --git a/src/types/GameTypes.ts b/src/types/GameTypes.ts index 782461b..9c3bca5 100644 --- a/src/types/GameTypes.ts +++ b/src/types/GameTypes.ts @@ -45,3 +45,5 @@ type WonInfo = { winner: PodiumPlayer | null allPlayers: PodiumPlayer[] } + +export type { GameInfo, LobbyInfo, TieInfo, TrumpInfo, WonInfo } diff --git a/src/types/authTypes.ts b/src/types/authTypes.ts deleted file mode 100644 index 85821e8..0000000 --- a/src/types/authTypes.ts +++ /dev/null @@ -1,16 +0,0 @@ - -type token = { - token: string - user: user -} -type user = { - id: number - name: string -} -type credentials = { - username: string - password: string -} - -export type { token, user, credentials } - diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue index 10ba0af..50997d6 100644 --- a/src/views/LoginView.vue +++ b/src/views/LoginView.vue @@ -30,6 +30,7 @@ style="width: 100%" filled dark + type="password" label-color="white" color="white" v-model="password" @@ -60,8 +61,8 @@ import { useQuasar } from 'quasar' import { ref } from 'vue' import axios from "axios"; -import {useAuthStore} from "@/stores/auth.ts"; import router from "@/router"; +import {useUserInfo} from "@/composables/useUserInfo.ts"; const api = window?.__RUNTIME_CONFIG__?.API_URL; @@ -71,14 +72,14 @@ const username = ref(null) const password = ref(null) const inProgress = ref(false) const loginError = ref('') +const uInfo = useUserInfo() const onSubmit = () => { if (inProgress.value) return inProgress.value = true loginError.value = '' - axios.post(`${api}/login`, {username: username.value, password: password.value}).then(response => { - const auth = useAuthStore() - auth.setToken(response.data.token) + axios.post(`${api}/login`, {username: username.value, password: password.value}, {withCredentials: true}).then((response) => { + uInfo.setUserInfo(response.data.user.username, response.data.user.id) router.push("/") }).catch(() => { loginError.value = 'Invalid username or password' @@ -107,11 +108,6 @@ const options = { }, "polygon": { "sides": 5 - }, - "image": { - "src": "img/github.svg", - "width": 100, - "height": 100 } }, "opacity": {