diff --git a/.env b/.env index d4797b7..15f1748 100644 --- a/.env +++ b/.env @@ -1,2 +1,2 @@ VITE_APP_TITLE=SynchIt -VITE_APP_WEBSTORAGE_NAMESPACE=Vuetify +VITE_APP_WEBSTORAGE_NAMESPACE=synchitApp diff --git a/components.d.ts b/components.d.ts index 0dbdb97..f404a5f 100644 --- a/components.d.ts +++ b/components.d.ts @@ -32,6 +32,7 @@ declare module '@vue/runtime-core' { VListItem: typeof import('vuetify/lib')['VListItem'] VListItemContent: typeof import('vuetify/lib')['VListItemContent'] VListItemIcon: typeof import('vuetify/lib')['VListItemIcon'] + VListItemSubtitle: typeof import('vuetify/lib')['VListItemSubtitle'] VListItemTitle: typeof import('vuetify/lib')['VListItemTitle'] VMain: typeof import('vuetify/lib')['VMain'] VNavigationDrawer: typeof import('vuetify/lib')['VNavigationDrawer'] diff --git a/package-lock.json b/package-lock.json index 41fd172..61064bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@logue/vue2-helpers": "^2.0.3", "@mdi/font": "^7.0.96", + "axios": "^1.1.2", "vue": "^2.7.10", "vue-class-component": "^7.2.6", "vue-property-decorator": "^9.1.2", @@ -1279,8 +1280,17 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.2.tgz", + "integrity": "sha512-bznQyETwElsXl2RK7HLLwb5GPpOLlycxHCtrpDR/4RqqBzjARaOTo3jz4IgtntWUYee7Ne4S8UHd92VCuzPaWA==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } }, "node_modules/balanced-match": { "version": "1.0.2", @@ -1611,7 +1621,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -1915,7 +1924,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -3447,6 +3455,25 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", @@ -3464,7 +3491,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -5170,7 +5196,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -5179,7 +5204,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -5815,6 +5839,11 @@ "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", "dev": true }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -8997,8 +9026,17 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.2.tgz", + "integrity": "sha512-bznQyETwElsXl2RK7HLLwb5GPpOLlycxHCtrpDR/4RqqBzjARaOTo3jz4IgtntWUYee7Ne4S8UHd92VCuzPaWA==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } }, "balanced-match": { "version": "1.0.2", @@ -9256,7 +9294,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -9497,8 +9534,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "dir-glob": { "version": "3.0.1", @@ -10544,6 +10580,11 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, "foreground-child": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", @@ -10558,7 +10599,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -11813,14 +11853,12 @@ "mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, "mime-types": { "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "requires": { "mime-db": "1.52.0" } @@ -12250,6 +12288,11 @@ "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", "dev": true }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", diff --git a/package.json b/package.json index 24172a6..cbf6fc1 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "dependencies": { "@logue/vue2-helpers": "^2.0.3", "@mdi/font": "^7.0.96", + "axios": "^1.1.2", "vue": "^2.7.10", "vue-class-component": "^7.2.6", "vue-property-decorator": "^9.1.2", diff --git a/src/App.vue b/src/App.vue index ec6639a..8b618a3 100644 --- a/src/App.vue +++ b/src/App.vue @@ -3,7 +3,7 @@ {{ title }} - + mdi-theme-light-dark + + + + + + {{ displayName }} + + nyior@nyior.com + + + + + Logout + + + + + + + + + Not Logged In + + + click continue to login or signup + + + + + + Continue + + + + + - - + + Home mdi-home-circle - + New Room mdi-forum-plus - + Account mdi-account-circle - - @@ -143,13 +175,23 @@ export default defineComponent({ /** Toggle Theme Dark/Light mode */ const themeDark: Ref = computed({ - get: () => store.getters['ConfigModule/themeDark'], - set: v => store.dispatch('ConfigModule/setThemeDark', v), + get: () => store.getters['config/themeDark'], + set: v => store.dispatch('config/setThemeDark', v), }); /** Error Message */ const error: ComputedRef = computed(() => store.getters.error); + /** Is user logged in? */ + const isAuthenticated: Ref = computed( + () => store.getters['user/isAuthenticated'] + ); + + /** User Display Name */ + const displayName: Ref = computed( + () => store.getters['user/displayName'] + ); + /** Modify snackbar text */ watch(snackbarText, () => (snackbar.value = true)); @@ -175,6 +217,11 @@ export default defineComponent({ } }); + const logout = () => { + // Update authentication state + store.dispatch('user/logoutAction'); + }; + /** Run once. */ onMounted(() => { document.title = title.value; @@ -192,6 +239,9 @@ export default defineComponent({ error, themeDark, value, + isAuthenticated, + displayName, + logout, }; }, }); @@ -208,7 +258,7 @@ html { scrollbar-color: map-get($grey, 'lighten-2') map-get($grey, 'base'); } -a{ +a { text-decoration: none !important; } diff --git a/src/components/Auth/LoginForm.vue b/src/components/Auth/LoginForm.vue index b8253e4..d85252a 100644 --- a/src/components/Auth/LoginForm.vue +++ b/src/components/Auth/LoginForm.vue @@ -5,6 +5,7 @@ lazy-validation class="text-center justify-center px-10" > + + - + submit diff --git a/src/components/Auth/SignupForm.vue b/src/components/Auth/SignupForm.vue index 187479e..d74a7a0 100644 --- a/src/components/Auth/SignupForm.vue +++ b/src/components/Auth/SignupForm.vue @@ -5,6 +5,7 @@ lazy-validation class="text-center justify-center px-10" > + + + submit @@ -56,39 +56,67 @@ diff --git a/src/main.ts b/src/main.ts index 2262029..6b96f5e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,12 +5,15 @@ import router from '@/router/router'; import store from '@/store'; import vuetify from './plugins/vuetify'; import teleport from 'vue2-teleport'; +import axiosResponseInterceptor from './utils/interceptors'; import App from '@/App.vue'; Vue.config.productionTip = false; Vue.component('Teleport', teleport); +axiosResponseInterceptor(store); + const app = new Vue({ router, store, diff --git a/src/middlewares/protected.ts b/src/middlewares/protected.ts index 16b5b6b..ecce66c 100644 --- a/src/middlewares/protected.ts +++ b/src/middlewares/protected.ts @@ -1,20 +1,19 @@ -// import type { -// NavigationGuardNext, -// RouteLocationNormalized -// } from 'vue-router' -// import store from '@/store' +import store from '@/store'; +/** + * protect necessary routes + * contains API calling utility functions + */ +function guardMyroute(to, from, next) { + const isAuthenticated = store.getters['user/isAuthenticated']; + console.log('AUTHENTICATED? :', isAuthenticated); + if (isAuthenticated) { + next(); // allow to enter route + } else { + // keep track of the route just before visiting the login page + const loginPath = window.location.pathname; + next({ name: 'auth', query: { redirect: loginPath } }); // go to '/login'; + } +} -// export default ( -// to: RouteLocationNormalized, -// from: RouteLocationNormalized, -// next: NavigationGuardNext -// ): void => { -// if (store.getters.getAuth) { -// next() -// } else { -// next('/') -// } -// } - -export {} +export { guardMyroute }; diff --git a/src/plugins/vuetify.ts b/src/plugins/vuetify.ts index 4546fb7..b860ce7 100644 --- a/src/plugins/vuetify.ts +++ b/src/plugins/vuetify.ts @@ -23,17 +23,17 @@ export default createVuetify({ theme: { themes: { dark: { - // primary: '#fed163', - primary: '#b390e9', - background: '#222831', - error: '#d63031', - info: '#0984e3', - secondary: '#fdcb6e', - success: '#00cec9', - surface: '#6c5ce7', - warning: '#FFDD93', - text: '#FFD369', - dominant: '#b390e9', + // primary: '#fed163', + primary: '#b390e9', + background: '#222831', + error: '#d63031', + info: '#0984e3', + secondary: '#fdcb6e', + success: '#00cec9', + surface: '#6c5ce7', + warning: '#FFDD93', + text: '#FFD369', + dominant: '#b390e9', }, light: { // primary: '#FFDD93', @@ -47,7 +47,7 @@ export default createVuetify({ warning: '#FFDD93', text: '#FFD369', dominant: '#6929cc', - }, + }, }, dark: true, // options: { @@ -63,7 +63,6 @@ export default createVuetify({ // customProperties: true, // }, }, - }); /** Create Vuetify */ diff --git a/src/router/router.ts b/src/router/router.ts index 6199551..ef6efe2 100644 --- a/src/router/router.ts +++ b/src/router/router.ts @@ -12,10 +12,11 @@ import type { VuetifyGoToTarget } from 'vuetify/types/services/goto'; import goTo from 'vuetify/lib/services/goto'; import store from '@/store'; -import HomePage from '@/views/Home/HomePage.vue' -import About from '@/views/Misc/AboutPage.vue' -import ErrorPage from '@/views/Misc/ErrorPage.vue' -import Auth from '@/views/Auth/Auth.vue' +import HomePage from '@/views/Home/HomePage.vue'; +import About from '@/views/Misc/AboutPage.vue'; +import ErrorPage from '@/views/Misc/ErrorPage.vue'; +import Auth from '@/views/Auth/Auth.vue'; +import { guardMyroute } from '@/middlewares/protected'; /** Router Config */ const routes: RouteRecordRaw[] = [ @@ -27,6 +28,7 @@ const routes: RouteRecordRaw[] = [ { path: '/about', name: 'about', + beforeEnter: guardMyroute, component: About, }, { diff --git a/src/store/ConfigModule.ts b/src/store/ConfigModule.ts deleted file mode 100644 index adae363..0000000 --- a/src/store/ConfigModule.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** Config store */ -import type { - ActionContext, - ActionTree, - GetterTree, - Module, - MutationTree, -} from 'vuex'; - -import type { RootState } from '.'; - -/** Config State */ -export interface ConfigState { - /** Dark Theme mode */ - themeDark: boolean; - /** Language */ - locale: string; -} - -/** Default Configure state value */ -const state: ConfigState = { - themeDark: window.matchMedia('(prefers-color-scheme: dark)').matches, - locale: - (window.navigator.languages && window.navigator.languages[0]) || - window.navigator.language, -}; - -/** Getters */ -const getters: GetterTree = { - themeDark: (s): boolean => s.themeDark, - locale: (s): string => s.locale, -}; - -/** Mutations */ -const mutations: MutationTree = { - storeThemeDark(s) { - s.themeDark = !s.themeDark; - }, - storeLocale(s, locale: string) { - s.locale = locale; - }, -}; - -/** Action */ -const actions: ActionTree = { - /** - * Switch Dark/Light mode. - * - * @param context - Vuex Context - */ - setThemeDark(context: ActionContext, mode: boolean) { - context.commit('storeThemeDark', mode); - }, - /** - * Change locale. - * - * @param context - Vuex Context - * @param locale - Locale code - */ - setLocale(context: ActionContext, locale: string) { - context.commit('storeLocale', locale); - }, -}; - -/** VuexStore */ -const ConfigModule: Module = { - namespaced: true, - state, - getters, - mutations, - actions, -}; - -export default ConfigModule; diff --git a/src/store/actions.ts b/src/store/actions.ts new file mode 100644 index 0000000..caad8bb --- /dev/null +++ b/src/store/actions.ts @@ -0,0 +1,48 @@ +import type { ActionContext, ActionTree } from 'vuex'; +import type { RootState } from './state'; + +/** Actions */ +export const actions: ActionTree = { + /** + * Loading overlay visibility + * + * @param context - Vuex Context + * @param display - Visibility + */ + setLoading( + context: ActionContext, + display: boolean = false + ) { + context.commit('storeLoading', display); + }, + /** + * Loading progress bar value + * + * @param context - Vuex Context + * @param progress - Percentage(0~100) + */ + setProgress( + context: ActionContext, + progress: number = 0 + ) { + context.commit('storeProgress', progress); + }, + /** + * Set snackbar message. + * + * @param context - Vuex Context + * @param message - Message text + */ + setMessage(context: ActionContext, message?: string) { + context.commit('storeMessage', message); + }, + /** + * Set Error message + * + * @param context - Vuex Context + * @param error - Error message etc. + */ + setError(context: ActionContext, error) { + context.commit('storeError', error); + }, +}; diff --git a/src/store/getters.ts b/src/store/getters.ts new file mode 100644 index 0000000..ed395f5 --- /dev/null +++ b/src/store/getters.ts @@ -0,0 +1,10 @@ +import type { GetterTree } from 'vuex'; +import type { RootState } from './state'; + +/** Getters */ +export const getters: GetterTree = { + loading: (s): boolean => s.loading, + progress: (s): number => s.progress, + message: (s): string | undefined => s.message, + error: (s): string | undefined => s.error, +}; diff --git a/src/store/index.ts b/src/store/index.ts index 790b45e..c549a91 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,130 +1,22 @@ -import type { - ActionContext, - ActionTree, - GetterTree, - MutationTree, - StoreOptions, -} from 'vuex'; -import { createStore } from '@logue/vue2-helpers/vuex'; -import VuexPersistence from 'vuex-persist'; - -// Modules -import ConfigModule from './ConfigModule'; +// Follow this tutorial to learn how to modularize the store +// https://medium.com/swlh/building-large-scale-applications-with-vuex-6d7e8ce0dfef -/** Root State Interface */ -export interface RootState { - /* + Loading overlay */ - loading: boolean; - /** ProgressBar Percentage */ - progress: number; - /** SnackBar Text */ - message?: string; - /** Error Message */ - error?: string; -} +// Also follow this Stackoverflow link -/** State Default value */ -const state: RootState = { - loading: false, - progress: 0, - message: undefined, - error: undefined, -}; +// https://stackoverflow.com/questions/64404599/how-to-modularize-vuex-into-different-folders-and-files -/** Getters */ -const getters: GetterTree = { - loading: (s): boolean => s.loading, - progress: (s): number => s.progress, - message: (s): string | undefined => s.message, - error: (s): string | undefined => s.error, -}; - -/** Mutations */ -const mutations: MutationTree = { - /** - * Store loading - * - * @param s - Vuex state - * @param display - Payload - */ - storeLoading(s, display: boolean) { - s.loading = display; - }, - /** - * Store progress - * - * @param s - Vuex state - * @param progress - Payload - */ - storeProgress(s, progress: number) { - s.progress = progress; - s.loading = true; - }, - /** - * Store snackbar text - * - * @param s - Vuex state - * @param message - Payload - */ - storeMessage(s, message: string) { - s.message = message; - }, - /** - * Store error message - * - * @param s - Vuex state - * @param error - Payload - */ - storeError(s, error: string) { - s.error = error; - }, -}; +import type { StoreOptions } from 'vuex'; +import { createStore } from '@logue/vue2-helpers/vuex'; +import VuexPersistence from 'vuex-persist'; +import type { RootState } from './state'; +import { state } from './state'; +import { getters } from './getters'; +import { mutations } from './mutations'; +import { actions } from './actions'; -/** Actions */ -const actions: ActionTree = { - /** - * Loading overlay visibility - * - * @param context - Vuex Context - * @param display - Visibility - */ - setLoading( - context: ActionContext, - display: boolean = false - ) { - context.commit('storeLoading', display); - }, - /** - * Loading progress bar value - * - * @param context - Vuex Context - * @param progress - Percentage(0~100) - */ - setProgress( - context: ActionContext, - progress: number = 0 - ) { - context.commit('storeProgress', progress); - }, - /** - * Set snackbar message. - * - * @param context - Vuex Context - * @param message - Message text - */ - setMessage(context: ActionContext, message?: string) { - context.commit('storeMessage', message); - }, - /** - * Set Error message - * - * @param context - Vuex Context - * @param error - Error message etc. - */ - setError(context: ActionContext, error) { - context.commit('storeError', error); - }, -}; +// Modules +import ConfigModule from './modules/config'; +import UserModule from './modules/user'; /** VuexStore */ const store: StoreOptions = { @@ -135,27 +27,15 @@ const store: StoreOptions = { mutations, actions, modules: { - ConfigModule, + config: ConfigModule, + user: UserModule, }, plugins: [ new VuexPersistence({ key: import.meta.env.VITE_APP_WEBSTORAGE_NAMESPACE || 'vuex', storage: window.localStorage, - modules: ['ConfigModule'], - }).plugin, - /* - // store as session storage - new VuexPersistence({ - key: import.meta.env.VITE_APP_WEBSTORAGE_NAMESPACE, - storage: window.sessionStorage, - modules: ['SomeModule'], + modules: ['config', 'user'], }).plugin, - // store as Indexed DB (using vuex-persist-indexeddb) - createPersistedState({ - key: import.meta.env.VITE_APP_WEBSTORAGE_NAMESPACE, - paths: ['SomeLargeModule'], - }), - */ ], }; diff --git a/src/store/modules/config/actions.ts b/src/store/modules/config/actions.ts new file mode 100644 index 0000000..0459db0 --- /dev/null +++ b/src/store/modules/config/actions.ts @@ -0,0 +1,25 @@ +/** Config store */ +import type { ActionContext, ActionTree } from 'vuex'; +import type { ConfigState } from './state'; +import type { RootState } from '@/store/state'; + +/** Action */ +export const actions: ActionTree = { + /** + * Switch Dark/Light mode. + * + * @param context - Vuex Context + */ + setThemeDark(context: ActionContext, mode: boolean) { + context.commit('storeThemeDark', mode); + }, + /** + * Change locale. + * + * @param context - Vuex Context + * @param locale - Locale code + */ + setLocale(context: ActionContext, locale: string) { + context.commit('storeLocale', locale); + }, +}; diff --git a/src/store/modules/config/getters.ts b/src/store/modules/config/getters.ts new file mode 100644 index 0000000..4349eac --- /dev/null +++ b/src/store/modules/config/getters.ts @@ -0,0 +1,10 @@ +/** Config store */ +import type { GetterTree } from 'vuex'; +import type { RootState } from '@/store/state'; +import type { ConfigState } from './state'; + +/** Getters */ +export const getters: GetterTree = { + themeDark: (s): boolean => s.themeDark, + locale: (s): string => s.locale, +}; diff --git a/src/store/modules/config/index.ts b/src/store/modules/config/index.ts new file mode 100644 index 0000000..218f6d0 --- /dev/null +++ b/src/store/modules/config/index.ts @@ -0,0 +1,18 @@ +import type { Module } from 'vuex'; +import type { ConfigState } from './state'; +import type { RootState } from '@/store/state'; +import { state } from './state'; +import { mutations } from './mutations'; +import { getters } from './getters'; +import { actions } from './actions'; + +/** VuexStore */ +const ConfigModule: Module = { + namespaced: true, + state, + getters, + mutations, + actions, +}; + +export default ConfigModule; diff --git a/src/store/modules/config/mutations.ts b/src/store/modules/config/mutations.ts new file mode 100644 index 0000000..2dd78db --- /dev/null +++ b/src/store/modules/config/mutations.ts @@ -0,0 +1,13 @@ +/** Config store */ +import type { MutationTree } from 'vuex'; +import type { ConfigState } from './state'; + +/** Mutations */ +export const mutations: MutationTree = { + storeThemeDark(s) { + s.themeDark = !s.themeDark; + }, + storeLocale(s, locale: string) { + s.locale = locale; + }, +}; diff --git a/src/store/modules/config/state.ts b/src/store/modules/config/state.ts new file mode 100644 index 0000000..7518652 --- /dev/null +++ b/src/store/modules/config/state.ts @@ -0,0 +1,15 @@ +/** Config State */ +export interface ConfigState { + /** Dark Theme mode */ + themeDark: boolean; + /** Language */ + locale: string; +} + +/** Default Configure state value */ +export const state: ConfigState = { + themeDark: window.matchMedia('(prefers-color-scheme: dark)').matches, + locale: + (window.navigator.languages && window.navigator.languages[0]) || + window.navigator.language, +}; diff --git a/src/store/modules/user/actions.ts b/src/store/modules/user/actions.ts new file mode 100644 index 0000000..debef47 --- /dev/null +++ b/src/store/modules/user/actions.ts @@ -0,0 +1,44 @@ +import type { ActionContext, ActionTree } from 'vuex'; +import type { UserState } from './state'; +import type { RootState } from '@/store/state'; + + +/** Action */ +export const actions: ActionTree = { + modifyAuthStateAction(context: ActionContext, payload) { + context.commit('modifyAuthState', payload); + }, + + modifyUserIdAction(context: ActionContext, payload) { + context.commit('modifyUserId', payload); + }, + + modifyAccessTokenAction( + context: ActionContext, + payload + ) { + context.commit('modifyAccessToken', payload); + }, + + modifyRefreshTokenAction( + context: ActionContext, + payload + ) { + context.commit('modifyRefreshToken', payload); + }, + + refreshAccessTokenAction( + context: ActionContext, + payload + ) { + context.commit('refreshAccessToken', payload); + }, + + logoutAction(context: ActionContext) { + context.commit('logout'); + }, + + loginAction: (context: ActionContext, payload) => { + context.commit('login', payload); + }, +}; diff --git a/src/store/modules/user/getters.ts b/src/store/modules/user/getters.ts new file mode 100644 index 0000000..94c9bcc --- /dev/null +++ b/src/store/modules/user/getters.ts @@ -0,0 +1,12 @@ +import type { GetterTree } from 'vuex'; +import type { RootState } from '@/store/state'; +import type { UserState } from './state'; + +/** Getters */ +export const getters: GetterTree = { + isAuthenticated: (state): boolean => state.isAuthenticated, + displayName: (state): string => state.displayName, + accessToken: (state): string => state.accessToken, + refreshToken: (state): string => state.refreshToken, + userId: (state): string => state.userId, +}; diff --git a/src/store/modules/user/index.ts b/src/store/modules/user/index.ts new file mode 100644 index 0000000..7d443fa --- /dev/null +++ b/src/store/modules/user/index.ts @@ -0,0 +1,18 @@ +import type { Module } from 'vuex'; +import type { UserState } from './state'; +import { state } from './state'; +import { getters } from './getters'; +import { mutations } from './mutations'; +import { actions } from './actions'; +import type { RootState } from '@/store/state'; + +/** VuexStore */ +const UserModule: Module = { + namespaced: true, + state, + getters, + mutations, + actions, +}; + +export default UserModule; diff --git a/src/store/modules/user/mutations.ts b/src/store/modules/user/mutations.ts new file mode 100644 index 0000000..44b509e --- /dev/null +++ b/src/store/modules/user/mutations.ts @@ -0,0 +1,48 @@ +import type { MutationTree } from 'vuex'; +import type { UserState } from './state'; + +/** Mutations */ +export const mutations: MutationTree = { + modifyAuthState: (state, payload) => { + state.isAuthenticated = payload; + }, + + modifyUserId: (state, payload) => { + state.userId = payload; + }, + + modifyAccessToken: (state, payload) => { + state.accessToken = payload; + }, + + modifyRefreshToken: (state, payload) => { + state.refreshToken = payload; + }, + + refreshAccessToken: (state, accessToken) => { + state.isAuthenticated = true; + state.accessToken = accessToken; + }, + + logout: state => { + state.isAuthenticated = false; + state.accessToken = ''; + state.refreshToken = ''; + state.userId = ''; + state.displayName = ''; + }, + + login: (state, payload) => { + const accessToken = payload.access_token; + const refreshToken = payload.refresh_token; + const userId = payload.uid; + const displayName = payload.display_name; + + state.isAuthenticated = true; + state.accessToken = accessToken; + state.refreshToken = refreshToken; + state.userId = userId; + state.displayName = displayName; + // then do all the route change here... router.push() + }, +}; diff --git a/src/store/modules/user/state.ts b/src/store/modules/user/state.ts new file mode 100644 index 0000000..25cc2a1 --- /dev/null +++ b/src/store/modules/user/state.ts @@ -0,0 +1,17 @@ +/** Root State Interface */ +export interface UserState { + isAuthenticated: boolean; + userId: string; + accessToken: string; + refreshToken: string; + displayName: string; +} + +/** State Default value */ +export const state: UserState = { + isAuthenticated: false, + userId: '', + accessToken: '', + refreshToken: '', + displayName: ' ', +}; diff --git a/src/store/mutations.ts b/src/store/mutations.ts new file mode 100644 index 0000000..2d092f2 --- /dev/null +++ b/src/store/mutations.ts @@ -0,0 +1,43 @@ +import type { MutationTree } from 'vuex'; +import type { RootState } from './state'; + +/** Mutations */ +export const mutations: MutationTree = { + /** + * Store loading + * + * @param s - Vuex state + * @param display - Payload + */ + storeLoading(s, display: boolean) { + s.loading = display; + }, + /** + * Store progress + * + * @param s - Vuex state + * @param progress - Payload + */ + storeProgress(s, progress: number) { + s.progress = progress; + s.loading = true; + }, + /** + * Store snackbar text + * + * @param s - Vuex state + * @param message - Payload + */ + storeMessage(s, message: string) { + s.message = message; + }, + /** + * Store error message + * + * @param s - Vuex state + * @param error - Payload + */ + storeError(s, error: string) { + s.error = error; + }, +}; diff --git a/src/store/state.ts b/src/store/state.ts new file mode 100644 index 0000000..736fcb6 --- /dev/null +++ b/src/store/state.ts @@ -0,0 +1,19 @@ +/** Root State Interface */ +export interface RootState { + /* + Loading overlay */ + loading: boolean; + /** ProgressBar Percentage */ + progress: number; + /** SnackBar Text */ + message?: string; + /** Error Message */ + error?: string; +} + +/** State Default value */ +export const state: RootState = { + loading: false, + progress: 0, + message: undefined, + error: undefined, +}; diff --git a/src/utils/apiService.ts b/src/utils/apiService.ts new file mode 100644 index 0000000..0a69c0e --- /dev/null +++ b/src/utils/apiService.ts @@ -0,0 +1,30 @@ +import instance from './axiosInstance'; +import store from '@/store'; + +/** + * Learn how to refresh tokens with axios interceptors + * contains API calling utility functions + */ +async function apiService(endpoint, method, data) { + const TKN = store.getters['user/accessToken']; + let token = ''; + + if (TKN !== undefined && TKN !== '' && TKN !== null) { + token = `Token ${TKN}`; + } + + const config = { + url: endpoint, + method: method, + data: data !== undefined ? data : null, + + headers: { + 'content-type': 'application/json', + Authorization: token, + }, + }; + + return instance(config).then(response => response.data); +} + +export { apiService }; diff --git a/src/utils/axiosInstance.ts b/src/utils/axiosInstance.ts new file mode 100644 index 0000000..c97e613 --- /dev/null +++ b/src/utils/axiosInstance.ts @@ -0,0 +1,12 @@ +import axios from 'axios'; + +const API_URL = 'http://127.0.0.1:8000'; + +const instance = axios.create({ + baseURL: API_URL, + headers: { + 'Content-Type': 'application/json', + }, +}); + +export default instance; diff --git a/src/utils/interceptors.ts b/src/utils/interceptors.ts new file mode 100644 index 0000000..71a6ea7 --- /dev/null +++ b/src/utils/interceptors.ts @@ -0,0 +1,37 @@ +import instance from './axiosInstance'; + +const axiosResponseInterceptor = store => { + const TKN = store.getters['user/accessToken']; + + instance.interceptors.response.use( + res => { + return res; + }, + async err => { + const originalConfig = err.config; + + // Access Token was expired + if (err.response.status === 401 && !originalConfig._retry) { + originalConfig._retry = true; + + try { + const rs = await instance.post('api/v1/auth/refresh-token', { + refreshToken: TKN, + }); + + const { accessToken } = rs.data; + + store.dispatch('user/modifyRefreshTokenAction', accessToken); + + return instance(originalConfig); + } catch (_error) { + return Promise.reject(_error); + } + } + + return Promise.reject(err); + } + ); +}; + +export default axiosResponseInterceptor; diff --git a/src/views/Auth/Auth.vue b/src/views/Auth/Auth.vue index fda116a..0367a22 100644 --- a/src/views/Auth/Auth.vue +++ b/src/views/Auth/Auth.vue @@ -1,9 +1,7 @@