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 @@
-
- Login or Signup
-
+ Login or Signup
- Login
- Signup
+ Login
+ Signup
-
+
-
+
@@ -32,27 +30,22 @@ import { defineComponent, reactive } from 'vue';
import LoginForm from '@/components/Auth/LoginForm.vue';
import SignupForm from '@/components/Auth/SignupForm.vue';
-
export default defineComponent({
components: {
LoginForm,
- SignupForm
+ SignupForm,
},
- props: {
-
- },
+ props: {},
setup(props) {
-
const state = reactive({
- tab: null
+ tab: null,
});
return {
- state
+ state,
};
- }
-
+ },
});
diff --git a/yarn.lock b/yarn.lock
index 01fe533..7416932 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -648,6 +648,15 @@
"resolved" "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
"version" "0.4.0"
+"axios@^1.1.2":
+ "integrity" "sha512-bznQyETwElsXl2RK7HLLwb5GPpOLlycxHCtrpDR/4RqqBzjARaOTo3jz4IgtntWUYee7Ne4S8UHd92VCuzPaWA=="
+ "resolved" "https://registry.npmjs.org/axios/-/axios-1.1.2.tgz"
+ "version" "1.1.2"
+ dependencies:
+ "follow-redirects" "^1.15.0"
+ "form-data" "^4.0.0"
+ "proxy-from-env" "^1.1.0"
+
"balanced-match@^1.0.0":
"integrity" "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
"resolved" "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
@@ -1763,6 +1772,11 @@
"resolved" "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz"
"version" "3.2.7"
+"follow-redirects@^1.15.0":
+ "integrity" "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
+ "resolved" "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz"
+ "version" "1.15.2"
+
"foreground-child@^2.0.0":
"integrity" "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA=="
"resolved" "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz"
@@ -3138,6 +3152,11 @@
"resolved" "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz"
"version" "1.2.4"
+"proxy-from-env@^1.1.0":
+ "integrity" "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ "resolved" "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
+ "version" "1.1.0"
+
"pseudomap@^1.0.2":
"integrity" "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ=="
"resolved" "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz"