From 368b996aafd21e959b2d0a4b391326fb03120200 Mon Sep 17 00:00:00 2001 From: Grzegorz envy Perzanowski Date: Mon, 20 May 2024 19:50:05 +0200 Subject: [PATCH] Added auth - login/logout/session (vue app) --- src/Overmoney.Portal/package-lock.json | 9 +++ src/Overmoney.Portal/package.json | 1 + .../src/components/generic/NavigationMenu.vue | 17 ++++- .../modals/UpdateTransactionModal.vue | 4 +- .../src/components/views/CategoryView.vue | 2 +- .../src/components/views/LoginView.vue | 34 +++++++++ .../src/data_access/authClient.ts | 24 +++++++ .../src/data_access/client.ts | 2 +- .../data_access/models/auth/loginResponse.ts | 6 ++ .../src/data_access/models/auth/profile.ts | 4 ++ .../src/data_access/sessionStore.ts | 66 ++++++++++++++++++ .../src/data_access/userContext.ts | 2 + src/Overmoney.Portal/src/main.ts | 2 + src/Overmoney.Portal/src/router.ts | 69 +++++++++++++++---- 14 files changed, 220 insertions(+), 22 deletions(-) create mode 100644 src/Overmoney.Portal/src/components/views/LoginView.vue create mode 100644 src/Overmoney.Portal/src/data_access/authClient.ts create mode 100644 src/Overmoney.Portal/src/data_access/models/auth/loginResponse.ts create mode 100644 src/Overmoney.Portal/src/data_access/models/auth/profile.ts create mode 100644 src/Overmoney.Portal/src/data_access/sessionStore.ts diff --git a/src/Overmoney.Portal/package-lock.json b/src/Overmoney.Portal/package-lock.json index 0a80bc8..7e064c3 100644 --- a/src/Overmoney.Portal/package-lock.json +++ b/src/Overmoney.Portal/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "axios": "^1.6.8", "pinia": "^2.1.7", + "pinia-plugin-persistedstate": "^3.2.1", "primevue": "^3.51.0", "vue": "^3.4.21", "vue-router": "^4.3.0" @@ -2552,6 +2553,14 @@ } } }, + "node_modules/pinia-plugin-persistedstate": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-3.2.1.tgz", + "integrity": "sha512-MK++8LRUsGF7r45PjBFES82ISnPzyO6IZx3CH5vyPseFLZCk1g2kgx6l/nW8pEBKxxd4do0P6bJw+mUSZIEZUQ==", + "peerDependencies": { + "pinia": "^2.0.0" + } + }, "node_modules/pinia/node_modules/vue-demi": { "version": "0.14.7", "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", diff --git a/src/Overmoney.Portal/package.json b/src/Overmoney.Portal/package.json index 4f6c858..8072a58 100644 --- a/src/Overmoney.Portal/package.json +++ b/src/Overmoney.Portal/package.json @@ -14,6 +14,7 @@ "dependencies": { "axios": "^1.6.8", "pinia": "^2.1.7", + "pinia-plugin-persistedstate": "^3.2.1", "primevue": "^3.51.0", "vue": "^3.4.21", "vue-router": "^4.3.0" diff --git a/src/Overmoney.Portal/src/components/generic/NavigationMenu.vue b/src/Overmoney.Portal/src/components/generic/NavigationMenu.vue index 14ef293..b8c0c8a 100644 --- a/src/Overmoney.Portal/src/components/generic/NavigationMenu.vue +++ b/src/Overmoney.Portal/src/components/generic/NavigationMenu.vue @@ -1,16 +1,17 @@ diff --git a/src/Overmoney.Portal/src/components/modals/UpdateTransactionModal.vue b/src/Overmoney.Portal/src/components/modals/UpdateTransactionModal.vue index e5548cc..deb9545 100644 --- a/src/Overmoney.Portal/src/components/modals/UpdateTransactionModal.vue +++ b/src/Overmoney.Portal/src/components/modals/UpdateTransactionModal.vue @@ -61,8 +61,8 @@ import type { Wallet } from '../../data_access/models/wallet' import type { Payee } from '../../data_access/models/payee' import type { Category } from '../../data_access/models/category' import type { Transaction } from '../../data_access/models/transaction' -import { updateTransactionRequest } from '@/data_access/models/requests/updateTransactionRequest' -import { PropType } from 'vue'; +import type { updateTransactionRequest } from '@/data_access/models/requests/updateTransactionRequest' +import type { PropType } from 'vue'; export default { props: { diff --git a/src/Overmoney.Portal/src/components/views/CategoryView.vue b/src/Overmoney.Portal/src/components/views/CategoryView.vue index 759bb6c..dd96525 100644 --- a/src/Overmoney.Portal/src/components/views/CategoryView.vue +++ b/src/Overmoney.Portal/src/components/views/CategoryView.vue @@ -58,7 +58,7 @@ export default { console.log("Category cannot be null"); } cat!.name = newName; - await this.client.updateCategory({ name: newName, userid: category.userId, id: category.id }); + await this.client.updateCategory({ name: newName, userId: category.userId, id: category.id }); } } }; diff --git a/src/Overmoney.Portal/src/components/views/LoginView.vue b/src/Overmoney.Portal/src/components/views/LoginView.vue new file mode 100644 index 0000000..cb96bc3 --- /dev/null +++ b/src/Overmoney.Portal/src/components/views/LoginView.vue @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/src/Overmoney.Portal/src/data_access/authClient.ts b/src/Overmoney.Portal/src/data_access/authClient.ts new file mode 100644 index 0000000..f059919 --- /dev/null +++ b/src/Overmoney.Portal/src/data_access/authClient.ts @@ -0,0 +1,24 @@ +import axios, { isCancel, AxiosError } from "axios"; +import type { LoginResponse } from "./models/auth/loginResponse"; +import type { UserProfile } from "./models/auth/profile"; + +export class AuthClient { + async loginUser(email: string, password: string) { + const response = await axios.post( + import.meta.env.VITE_API_URL + + `Identity/login?useCookies=false&useSessionCookies=false`, + { email, password } + ); + return response.data; + } + + async getUserProfile(token: string) { + const response = await axios.get( + import.meta.env.VITE_API_URL + `users/profile`, { + headers: { + "Authorization": `Bearer ${token}` + } + }); + return response.data; + } +} \ No newline at end of file diff --git a/src/Overmoney.Portal/src/data_access/client.ts b/src/Overmoney.Portal/src/data_access/client.ts index b189907..bf6eac0 100644 --- a/src/Overmoney.Portal/src/data_access/client.ts +++ b/src/Overmoney.Portal/src/data_access/client.ts @@ -2,7 +2,7 @@ import axios, { isCancel, AxiosError } from "axios"; import type { Category } from "./models/category"; import type { Payee } from "./models/payee"; import type { createCategoryRequest } from "./models/requests/createCategoryRequest"; -import type { updateCategoryRequest } from "./models/requests/createCategoryRequest"; +import type { updateCategoryRequest } from "./models/requests/updateCategoryRequest"; import type { createPayeeReqeuest } from "./models/requests/createPayeeReqeuest"; import type { updatePayeeRequest } from "./models/requests/updatePayeeRequest"; import type { createTransactionRequest } from "./models/requests/createTransactionRequest"; diff --git a/src/Overmoney.Portal/src/data_access/models/auth/loginResponse.ts b/src/Overmoney.Portal/src/data_access/models/auth/loginResponse.ts new file mode 100644 index 0000000..13bc2bd --- /dev/null +++ b/src/Overmoney.Portal/src/data_access/models/auth/loginResponse.ts @@ -0,0 +1,6 @@ +export type LoginResponse = { + tokenType: string, + accessToken: string, + expiresIn: number, + refreshToken: string +} \ No newline at end of file diff --git a/src/Overmoney.Portal/src/data_access/models/auth/profile.ts b/src/Overmoney.Portal/src/data_access/models/auth/profile.ts new file mode 100644 index 0000000..6339325 --- /dev/null +++ b/src/Overmoney.Portal/src/data_access/models/auth/profile.ts @@ -0,0 +1,4 @@ +export type UserProfile = { + id: number, + email: string +} \ No newline at end of file diff --git a/src/Overmoney.Portal/src/data_access/sessionStore.ts b/src/Overmoney.Portal/src/data_access/sessionStore.ts new file mode 100644 index 0000000..a272527 --- /dev/null +++ b/src/Overmoney.Portal/src/data_access/sessionStore.ts @@ -0,0 +1,66 @@ +import { defineStore } from "pinia"; +import type { UserContext } from "./userContext"; +import { AuthClient } from "./authClient"; + +export const userSessionStore = defineStore("user", { + state: () => { + return { + userContext: null as UserContext | null, + }; + }, + getters: { + apiToken(): string | undefined { + return this.userContext?.token; + }, + getUserId(): number | undefined { + return this.userContext?.userId; + }, + isAuthenticated(): boolean { + //no token + if (this.userContext === null || this.userContext === undefined) { + return false; + } + + //no token + if (this.userContext?.token === null) { + return false; + } + + //token expired + if (this.userContext?.expiresOn <= new Date()) { + return false; + } + + return true; + }, + }, + actions: { + async loginUser(email: string, password: string): Promise { + const client = new AuthClient(); + try { + const authResponse = await client.loginUser(email, password); + + const profileResponse = await client.getUserProfile( + authResponse.accessToken + ); + + this.userContext = { + token: authResponse.accessToken, + userId: profileResponse.id, + expiresOn: new Date( + new Date().getTime() + authResponse.expiresIn * 1000 + ), + }; + + return true; + } catch (error) { + console.log(error); + return false; + } + }, + logoutUser() { + this.$reset(); + }, + }, + persist: true, +}); diff --git a/src/Overmoney.Portal/src/data_access/userContext.ts b/src/Overmoney.Portal/src/data_access/userContext.ts index a223bcc..1593969 100644 --- a/src/Overmoney.Portal/src/data_access/userContext.ts +++ b/src/Overmoney.Portal/src/data_access/userContext.ts @@ -1,3 +1,5 @@ export type UserContext = { userId: number; + token: string; + expiresOn: Date } \ No newline at end of file diff --git a/src/Overmoney.Portal/src/main.ts b/src/Overmoney.Portal/src/main.ts index 51b5515..52e1c40 100644 --- a/src/Overmoney.Portal/src/main.ts +++ b/src/Overmoney.Portal/src/main.ts @@ -1,9 +1,11 @@ import { createApp } from 'vue' import { createPinia } from 'pinia'; +import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; import App from './App.vue' import router from './router'; const pinia = createPinia(); +pinia.use(piniaPluginPersistedstate); const app = createApp(App); app.use(router); diff --git a/src/Overmoney.Portal/src/router.ts b/src/Overmoney.Portal/src/router.ts index ac39892..7768a84 100644 --- a/src/Overmoney.Portal/src/router.ts +++ b/src/Overmoney.Portal/src/router.ts @@ -1,22 +1,61 @@ -import { createMemoryHistory, createRouter } from 'vue-router' +import { createRouter, createWebHistory } from "vue-router"; +import axios from "axios"; +import { userSessionStore } from "./data_access/sessionStore"; -import MainView from './components/views/testComp.vue' -import PayeeView from './components/views/PayeeView.vue' -import CategoryView from './components/views/CategoryView.vue' -import TransactionView from './components/views/TransactionView.vue' -import SettingsView from './components/views/SettingsView.vue' +import MainView from "./components/views/testComp.vue"; +import PayeeView from "./components/views/PayeeView.vue"; +import CategoryView from "./components/views/CategoryView.vue"; +import TransactionView from "./components/views/TransactionView.vue"; +import SettingsView from "./components/views/SettingsView.vue"; +import LoginView from "./components/views/LoginView.vue"; const routes = [ - { path: '/', component: MainView }, - { path: '/payees', component: PayeeView }, - { path: '/categories', component: CategoryView }, - { path: '/transactions', component: TransactionView }, - { path: '/settings', component: SettingsView } -] + { path: "/login", component: LoginView, meta: { requiresAuth: false } }, + { path: "/", component: MainView, meta: { requiresAuth: true } }, + { path: "/payees", component: PayeeView, meta: { requiresAuth: true } }, + { + path: "/categories", + component: CategoryView, + meta: { requiresAuth: true }, + }, + { + path: "/transactions", + component: TransactionView, + meta: { requiresAuth: true }, + }, + { path: "/settings", component: SettingsView, meta: { requiresAuth: true } }, +]; const router = createRouter({ - history: createMemoryHistory(), + history: createWebHistory(), routes, -}) +}); -export default router; \ No newline at end of file +const setAuthorization = (token: string) => { + axios.defaults.headers.common.Authorization = `Bearer ${token}`; +}; + +router.beforeEach((to, from, next) => { + const requiresAuth = to.matched.some((record) => record.meta.requiresAuth); + const session = userSessionStore(); + if (session.isAuthenticated) { + setAuthorization(session.apiToken!); + } + if (requiresAuth && !session.isAuthenticated) { + next("/login"); + } else if (to.path === "/login" && session.isAuthenticated) { + next("/"); + } else { + next(); + } +}); + +axios.interceptors.response.use(null, (error) => { + if (error.response.status == 403 || error.response.status == 401) { + router.push("/login"); + } + + return Promise.reject(error); +}); + +export default router;