Skip to content

Commit 628a477

Browse files
feat: vue-route-guard library (#109)
* added Vue integration to nx * added vue-route-guard lib Co-authored-by: Chris Trzesniewski <k.trzesniewski@gmail.com>
1 parent 961f295 commit 628a477

23 files changed

+5614
-1145
lines changed

.eslintrc.json

+8
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@
1616
"sourceTag": "framework:angular",
1717
"onlyDependOnLibsWithTags": ["framework:angular", "framework:none"]
1818
},
19+
{
20+
"sourceTag": "framework:vue",
21+
"onlyDependOnLibsWithTags": ["framework:vue", "framework:none"]
22+
},
23+
{
24+
"sourceTag": "framework:react",
25+
"onlyDependOnLibsWithTags": ["framework:react", "framework:none"]
26+
},
1927
{
2028
"sourceTag": "framework:none",
2129
"onlyDependOnLibsWithTags": ["framework:none"]

.vscode/extensions.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
"angular.ng-template",
44
"nrwl.angular-console",
55
"esbenp.prettier-vscode",
6-
"firsttris.vscode-jest-runner"
6+
"firsttris.vscode-jest-runner",
7+
"johnsoncodehk.volar",
8+
"samatech.postcss-vue"
79
]
810
}

apps/.gitkeep

Whitespace-only changes.

libs/.gitkeep

Whitespace-only changes.

libs/vue-route-guard/.eslintrc.js

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
module.exports = {
2+
extends: ['../../.eslintrc.json'],
3+
ignorePatterns: ['!**/*'],
4+
overrides: [
5+
{
6+
files: ['*.ts', '*.vue'],
7+
extends: [
8+
'plugin:vue/vue3-recommended',
9+
'plugin:@intlify/vue-i18n/recommended',
10+
'@vue/typescript/recommended',
11+
'prettier',
12+
],
13+
plugins: ['import'],
14+
parser: 'vue-eslint-parser',
15+
parserOptions: { ecmaVersion: 2020 },
16+
rules: {
17+
'no-unused-vars': 'off',
18+
'no-console': 'off',
19+
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
20+
'space-before-function-paren': [
21+
'error',
22+
{ anonymous: 'never', named: 'never', asyncArrow: 'always' },
23+
],
24+
'padded-blocks': 'off',
25+
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
26+
'no-multiple-empty-lines': ['error', { max: 1 }],
27+
'keyword-spacing': ['error', { after: true }],
28+
'max-len': ['error', { code: 100, ignorePattern: '^\\s*<path' }],
29+
'no-param-reassign': [2, { props: false }],
30+
'object-curly-newline': [
31+
'error',
32+
{
33+
consistent: true,
34+
multiline: true,
35+
},
36+
],
37+
'no-extra-boolean-cast': 'error',
38+
'import/extensions': [
39+
'error',
40+
{
41+
ts: 'never',
42+
js: 'never',
43+
vue: 'always',
44+
json: 'always',
45+
png: 'always',
46+
jpg: 'always',
47+
mp3: 'always',
48+
mp4: 'always',
49+
},
50+
],
51+
indent: ['error', 2, { SwitchCase: 0 }],
52+
'vue/attribute-hyphenation': ['error', 'never'],
53+
'vue/singleline-html-element-content-newline': [
54+
'error',
55+
{
56+
ignoreWhenNoAttributes: true,
57+
ignoreWhenEmpty: true,
58+
},
59+
],
60+
'vue/max-attributes-per-line': [
61+
'error',
62+
{
63+
singleline: 3,
64+
},
65+
],
66+
'vue/no-v-html': 'off',
67+
'@intlify/vue-i18n/no-v-html': 'off',
68+
'@intlify/vue-i18n/no-unused-keys': [
69+
'error',
70+
{
71+
src: './src',
72+
extensions: ['.js', '.vue'],
73+
},
74+
],
75+
'@intlify/vue-i18n/no-raw-text': 'error',
76+
},
77+
},
78+
],
79+
};

libs/vue-route-guard/jest.config.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/* eslint-disable */
2+
export default {
3+
displayName: 'vue-route-guard',
4+
preset: '../../jest.preset.js',
5+
transform: {
6+
'^.+\\.[tj]s$': 'babel-jest',
7+
},
8+
moduleFileExtensions: ['ts', 'js', 'html'],
9+
coverageDirectory: '../../coverage/libs/vue-route-guard',
10+
};

libs/vue-route-guard/package.json

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "@this-dot/vue-route-guard",
3+
"version": "0.0.1",
4+
"description": "Vuejs helper library for adding guards to route",
5+
"license": "MIT",
6+
"author": "This Dot Labs",
7+
"files": [
8+
"src"
9+
],
10+
"keywords": [
11+
"vue",
12+
"vuejs",
13+
"vue route",
14+
"vue router"
15+
],
16+
"peerDependencies": {
17+
"vue": "^3.2.37",
18+
"vue-router": "^4.1.2"
19+
}
20+
}

libs/vue-route-guard/project.json

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
3+
"projectType": "library",
4+
"sourceRoot": "libs/vue-route-guard/src",
5+
"targets": {
6+
"build": {
7+
"executor": "nx-vue3-vite:build-app",
8+
"options": {
9+
"dist": "dist/libs/vue-route-guard"
10+
}
11+
},
12+
"lint": {
13+
"executor": "@nrwl/linter:eslint",
14+
"options": {
15+
"lintFilePatterns": ["libs/vue-route-guard/**/*.{js,jsx,ts,tsx,vue}"]
16+
}
17+
},
18+
"test": {
19+
"executor": "@nrwl/jest:jest",
20+
"outputs": ["coverage/libs/vue-route-guard"],
21+
"options": {
22+
"jestConfig": "libs/vue-route-guard/jest.config.ts",
23+
"passWithNoTests": true
24+
}
25+
}
26+
},
27+
"tags": ["framework:vue"]
28+
}

libs/vue-route-guard/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './lib';

libs/vue-route-guard/src/lib/guard.ts

+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import * as Vue from 'vue';
2+
import { Router } from 'vue-router';
3+
import { isArrayIntersecting } from './helpers';
4+
import {
5+
GuardConfig,
6+
GuardConfigOptions,
7+
GuardConfigRedirect,
8+
GuardConfigStore,
9+
GuardConfigToken,
10+
} from './types';
11+
import Storage from './storage';
12+
13+
const defaultOptions = {
14+
permissionKey: 'permission',
15+
};
16+
17+
let guardStore: GuardConfigStore;
18+
19+
export default class Guard {
20+
public $store: GuardConfigStore;
21+
static router: Router;
22+
static tokenConfig: GuardConfigToken;
23+
static redirect: GuardConfigRedirect;
24+
static options: GuardConfigOptions;
25+
static storage: Storage;
26+
static packageKey: string;
27+
28+
constructor(config: GuardConfig, packageKey: string) {
29+
if (!config.token?.name) {
30+
throw Error('@thisdot/vue-route-guard: Token name is not set');
31+
}
32+
33+
Guard.router = config.router;
34+
Guard.tokenConfig = config.token;
35+
Guard.redirect = config.redirect || {};
36+
Guard.options = Object.assign({}, defaultOptions, config.options);
37+
Guard.storage = new Storage(config.token.storage);
38+
Guard.packageKey = packageKey;
39+
40+
guardStore = Vue.reactive({
41+
state: {
42+
loading: false,
43+
authentication: null,
44+
isAuthenticated: false,
45+
},
46+
});
47+
48+
this.$store = guardStore;
49+
50+
Guard.initializeAuthentication();
51+
52+
Guard.router.beforeEach((guard) => {
53+
// check if route requires authentication and authentication exists
54+
if (guard.meta.requiresAuth && !guardStore.state.isAuthenticated) {
55+
return Guard.redirect.noAuthentication || false;
56+
}
57+
58+
// check if route requires authentication and authentication access exists
59+
if (guard.meta.requiresAuth && !this.hasAuthenticationAccess(guard.meta.access)) {
60+
return Guard.redirect.noPermission || Guard.redirect.noAuthentication || false;
61+
}
62+
63+
return true;
64+
});
65+
}
66+
67+
public install(app: Vue.App) {
68+
app.config.globalProperties[`$${Guard.packageKey}`] = this;
69+
70+
app.provide(Guard.packageKey, this);
71+
}
72+
73+
public token() {
74+
return Guard.storage.get(Guard.tokenConfig.name);
75+
}
76+
77+
public isAuthenticated() {
78+
return guardStore.state.isAuthenticated;
79+
}
80+
81+
public hasAuthenticationAccess(permission: string[] = []) {
82+
const permissionKey = Guard.options.permissionKey;
83+
if (
84+
guardStore.state.isAuthenticated === true &&
85+
(permission.length === 0 ||
86+
(permission.length > 0 &&
87+
permissionKey &&
88+
guardStore.state.authentication &&
89+
guardStore.state.authentication[permissionKey] &&
90+
isArrayIntersecting(
91+
permission,
92+
guardStore.state.authentication[permissionKey] as string[]
93+
)))
94+
) {
95+
return true;
96+
}
97+
98+
return false;
99+
}
100+
101+
public async clearAuthentication() {
102+
Guard.clearAuthenticationState();
103+
Guard.clearToken();
104+
105+
const redirectRoute = Guard.redirect.clearAuthentication || Guard.redirect.noAuthentication;
106+
if (redirectRoute) {
107+
Guard.router.push(redirectRoute);
108+
}
109+
return true;
110+
}
111+
112+
public async refreshAuthentication() {
113+
await Guard.initializeAuthentication();
114+
return true;
115+
}
116+
117+
public async setToken({ token }: { token: string }) {
118+
Guard.storage.set(Guard.tokenConfig.name, token);
119+
await Guard.initializeAuthentication();
120+
return true;
121+
}
122+
123+
static isTokenAvailable() {
124+
return !!Guard.storage.get(Guard.tokenConfig.name);
125+
}
126+
127+
static clearToken() {
128+
Guard.storage.remove(Guard.tokenConfig.name);
129+
}
130+
131+
static setAuthenticationState(authentication: { [key: string]: unknown }) {
132+
guardStore.state.loading = false;
133+
guardStore.state.authentication = authentication;
134+
guardStore.state.isAuthenticated = true;
135+
}
136+
137+
static clearAuthenticationState() {
138+
guardStore.state.loading = false;
139+
guardStore.state.authentication = null;
140+
guardStore.state.isAuthenticated = false;
141+
}
142+
143+
static async initializeAuthentication() {
144+
// check if token exists then set authentication
145+
if (Guard.isTokenAvailable() && typeof Guard.options.fetchAuthentication === 'function') {
146+
guardStore.state.loading = true;
147+
const auntenticationData = await Guard.options.fetchAuthentication();
148+
Guard.setAuthenticationState(auntenticationData);
149+
}
150+
return true;
151+
}
152+
}
+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import * as Vue from 'vue';
2+
import { StorageType } from './types';
3+
4+
/**
5+
* Return true if 2 arrays have any common value
6+
*
7+
* @param {string[]} arr1
8+
* @param {string[]} arr2
9+
*
10+
* @returns {boolean}
11+
*/
12+
export function isArrayIntersecting(arr1: string[], arr2: string[]) {
13+
for (let i = 0; i < arr1.length; i++) {
14+
const role = arr1[i];
15+
if (arr2.indexOf(role) >= 0) {
16+
return true;
17+
}
18+
}
19+
return false;
20+
}
21+
22+
/**
23+
* Return Vue version
24+
*
25+
* @param {Vue} app
26+
*
27+
* @returns {?number}
28+
*/
29+
export function getVueVersion(app: typeof Vue): number {
30+
return Number(app.version?.split('.')[0]);
31+
}
32+
33+
/**
34+
* Return if storage is available
35+
*
36+
* @returns {boolean}
37+
*/
38+
export function isStorageAvailable(storage: StorageType): boolean {
39+
const storageValue = window[storage];
40+
if (
41+
!!storageValue ||
42+
typeof storageValue !== 'object' ||
43+
typeof (storageValue as Storage).setItem !== 'function' ||
44+
typeof (storageValue as Storage).removeItem !== 'function'
45+
) {
46+
return false;
47+
}
48+
49+
try {
50+
window[storage].setItem('vue-route-guard', 'vue-route-guard');
51+
window[storage].removeItem('vue-route-guard');
52+
return true;
53+
} catch (e) {
54+
return false;
55+
}
56+
}

libs/vue-route-guard/src/lib/index.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as Vue from 'vue';
2+
3+
import Guard from './guard';
4+
import { getVueVersion } from './helpers';
5+
import { GuardConfig } from './types';
6+
7+
const packageKey = 'guard';
8+
9+
export function setupGuard(options: GuardConfig) {
10+
if (getVueVersion(Vue) < 2) {
11+
throw new Error('@thisdot/vue-route-guard: Vue 2 is not supported');
12+
}
13+
14+
return new Guard(options, packageKey);
15+
}
16+
17+
export function useGuard() {
18+
return Vue.inject(packageKey);
19+
}

0 commit comments

Comments
 (0)