Guest User

Auth Example

a guest
Oct 1st, 2018
53
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /**
  2.  * Vuex
  3.  */
  4.  
  5. import {
  6.   AUTH_NAMESPACE,
  7.   AuthGetterTypes,
  8.   AuthActionTypes,
  9. } from '@/store/modules/auth';
  10.  
  11. /**
  12.  * Data
  13.  */
  14.  
  15. import PageNames from '@/data/enum/page-names';
  16. import Statuses from '@/data/enum/generic-statuses';
  17. import Roles from '@/data/enum/roles';
  18. import API_CONFIG from '@/data/api-config';
  19.  
  20. /**
  21.  * Utils
  22.  */
  23.  
  24. import axios from 'axios';
  25. import { findLast, get } from 'lodash';
  26. import { inNavRoutes } from '@/router';
  27.  
  28.  
  29. /**
  30.  * @desc Will store an instance of VueRouter after applying the auth-plugin.
  31.  *
  32.  * @type {WeakMap<Object, any>}
  33.  */
  34. const routerInstance = new WeakMap();
  35.  
  36. /**
  37.  * @desc Will store an instance of Vuex after applying the auth-plugin.
  38.  *
  39.  * @type {WeakMap<Object, any>}
  40.  */
  41. const storeInstance = new WeakMap();
  42.  
  43. /* eslint-disable dot-notation */
  44.  
  45. export default {
  46.   /**
  47.    * @desc Install method to register plugin with VueJS.
  48.    *
  49.    * @param {Object} Vue
  50.    * @param {Object} router
  51.    * @param {Object} store
  52.    */
  53.   install(Vue, {
  54.     router,
  55.     store,
  56.   }) {
  57.     /**
  58.      * @desc Interceptor to set Auth header.
  59.      */
  60.     axios.interceptors.request.use((config) => {
  61.       const authUrl = `${API_CONFIG.XXX.HOST}${API_CONFIG.XXX.ENDPOINTS.AUTH.CREATE_TOKEN.path}`;
  62.  
  63.       if (config.url !== authUrl) {
  64.         const accessToken = store.getters[`${AUTH_NAMESPACE}/${AuthGetterTypes.accessToken}`];
  65.         const hasAuthHeader = !!config['Authorization'];
  66.  
  67.         if (accessToken && !hasAuthHeader) {
  68.           this.$_setAuthHeader(config);
  69.         }
  70.       }
  71.  
  72.       return config;
  73.     });
  74.  
  75.     /**
  76.      * @desc Interceptor to refresh token if it has been overdue.
  77.      *
  78.      * @modifies {refreshCounter}
  79.      */
  80.     axios.interceptors.response.use(
  81.       response => response,
  82.       async (error) => {
  83.         if (this.$_isInvalidToken(error)) {
  84.           await this.$_refreshToken(error);
  85.  
  86.           return this.$_retryRequest(error.config);
  87.         }
  88.  
  89.         throw error;
  90.       },
  91.     );
  92.  
  93.     /**
  94.      * @desc Guard to check does user granted to see the page.
  95.      */
  96.     router.beforeEach(async (to, from, next) => {
  97.       try {
  98.         const hasTokens = store.getters[`${AUTH_NAMESPACE}/${AuthGetterTypes.hasTokens}`];
  99.  
  100.         if (hasTokens && !this.checkRole()) {
  101.           await store.dispatch(`${AUTH_NAMESPACE}/${AuthActionTypes.loadUser}`);
  102.  
  103.           store.dispatch(`${AUTH_NAMESPACE}/${AuthActionTypes.setAuthFulfilled}`);
  104.         }
  105.  
  106.         if (this.checkRole() && to.name === PageNames.SIGN_IN) {
  107.           return next({ name: PageNames.DASHBOARD });
  108.         } else if (to.name !== PageNames.SIGN_IN) {
  109.           this.$_routeCheckAuth(to, from, next);
  110.         }
  111.  
  112.         return next();
  113.       } catch (error) {
  114.         next({
  115.           name: PageNames.SIGN_IN,
  116.         });
  117.  
  118.         throw error;
  119.       }
  120.     });
  121.  
  122.     /**
  123.      * @desc Guard to close all notifications and messages before transition to another route.
  124.      */
  125.     router.beforeEach((to, from, next) => {
  126.       document.title = to.name;
  127.  
  128.       /**
  129.        * @desc Close all notifications.
  130.        * @type {NodeListOf<Element>}
  131.        */
  132.  
  133.       const notificationCloseButtons = document.querySelectorAll('.el-notification__closeBtn');
  134.  
  135.       if (notificationCloseButtons.length) {
  136.         notificationCloseButtons.forEach((closeButton) => {
  137.           closeButton.click();
  138.         });
  139.       }
  140.  
  141.       /**
  142.        * @desc Close all messages.
  143.        * @type {NodeListOf<Element>}
  144.        */
  145.  
  146.       const messageCloseButtons = document.querySelectorAll('.el-message__closeBtn');
  147.  
  148.       if (messageCloseButtons.length) {
  149.         messageCloseButtons.forEach((closeButton) => {
  150.           closeButton.click();
  151.         });
  152.       }
  153.  
  154.       next();
  155.     });
  156.  
  157.     routerInstance.set(this, router);
  158.     storeInstance.set(this, store);
  159.  
  160.     // eslint-disable-next-line no-param-reassign
  161.     Vue.prototype.$auth = this;
  162.     // eslint-disable-next-line no-param-reassign
  163.     Vue.auth = this;
  164.   },
  165.  
  166.   /**
  167.    * @desc Perform sign in.
  168.    *
  169.    * @param {Object} credentials
  170.    * @param {string|Object} redirect
  171.    * @returns {Promise}
  172.    */
  173.   async signIn(credentials, redirect) {
  174.     let store = null;
  175.     let router = null;
  176.  
  177.     try {
  178.       store = storeInstance.get(this);
  179.       router = routerInstance.get(this);
  180.  
  181.       store.dispatch(`${AUTH_NAMESPACE}/${AuthActionTypes.setAuthPending}`);
  182.  
  183.       const { username, password } = credentials;
  184.  
  185.       const response = await this.$_passwordGrant(username, password);
  186.  
  187.       this.$_storeTokens(response.access_token, response.refresh_token);
  188.  
  189.       await store.dispatch(`${AUTH_NAMESPACE}/${AuthActionTypes.loadUser}`);
  190.  
  191.       store.dispatch(`${AUTH_NAMESPACE}/${AuthActionTypes.setAuthFulfilled}`);
  192.  
  193.       if (!this.checkRole(Roles.EDITOR)) {
  194.         this.signOut();
  195.  
  196.         throw new Error('The user credentials were incorrect.');
  197.       }
  198.  
  199.       router.push(redirect || {
  200.         name: PageNames.DASHBOARD,
  201.       });
  202.  
  203.       return true;
  204.     } catch (error) {
  205.       if (process.env.VUE_APP_CONSOLE_MESSAGES === 'on') {
  206.         console.warn('[Auth] An error occurred while signin in.', error || '(no data)');
  207.       }
  208.  
  209.       store.dispatch(`${AUTH_NAMESPACE}/${AuthActionTypes.setAuthRejected}`);
  210.  
  211.       throw error;
  212.     }
  213.   },
  214.  
  215.   /**
  216.    * @desc Perform sign out.
  217.    *
  218.    * @param {string|Object} redirect
  219.    */
  220.   signOut(redirect) {
  221.     const store = storeInstance.get(this);
  222.     const router = routerInstance.get(this);
  223.  
  224.     store.dispatch(`${AUTH_NAMESPACE}/${AuthActionTypes.resetAuthState}`);
  225.  
  226.     router.push(redirect || `/${PageNames.SIGN_IN}`);
  227.   },
  228.  
  229.   /**
  230.    * @desc Get the currently authenticated user.
  231.    *
  232.    * @returns {Object|null}
  233.    */
  234.   getUser() {
  235.     const store = storeInstance.get(this);
  236.  
  237.     return store.getters[`${AUTH_NAMESPACE}/${AuthGetterTypes.user}`];
  238.   },
  239.  
  240.   /**
  241.    * @desc Check if the currently authenticated user has a specific role.
  242.    *
  243.    * @param {string} role
  244.    * @returns {boolean}
  245.    */
  246.   checkRole(role) {
  247.     const store = storeInstance.get(this);
  248.     const user = this.getUser();
  249.     const isAuthFulfilled = store.getters[`${AUTH_NAMESPACE}/${AuthGetterTypes.authStatus}`] === Statuses.FULFILLED;
  250.  
  251.     if (user === null || isAuthFulfilled === false) {
  252.       return false;
  253.     }
  254.  
  255.     if (typeof role === 'string') {
  256.       const { granted } = user;
  257.  
  258.       if (granted) {
  259.         return granted.findIndex(role_ => role_ === role) !== -1;
  260.       }
  261.  
  262.       return false;
  263.     }
  264.  
  265.     return true;
  266.   },
  267.  
  268.   /**
  269.    * @desc Refresh the current token.
  270.    *
  271.    * @param {*} error
  272.    * @modifies {refreshCounter}
  273.    * @returns {Promise}
  274.    */
  275.   async $_refreshToken(error) {
  276.     let store = null;
  277.  
  278.     try {
  279.       store = storeInstance.get(this);
  280.       const hasTokens = store.getters[`${AUTH_NAMESPACE}/${AuthGetterTypes.hasTokens}`];
  281.  
  282.       if (!hasTokens) {
  283.         throw error;
  284.       }
  285.  
  286.       const response = await this.$_refreshTokenGrant(store.getters[`${AUTH_NAMESPACE}/${AuthGetterTypes.refreshToken}`]);
  287.  
  288.       if (response) {
  289.         this.$_storeTokens(response.access_token, response.refresh_token);
  290.  
  291.         return true;
  292.       }
  293.  
  294.       return false;
  295.     } catch (error_) {
  296.       if (process.env.VUE_APP_CONSOLE_MESSAGES === 'on') {
  297.         console.warn('[Auth] An error occurred while refreshing token.', error_ || '(no data)');
  298.       }
  299.  
  300.       this.signOut();
  301.  
  302.       throw error_;
  303.     }
  304.   },
  305.  
  306.   /**
  307.    * @desc Retry a request.
  308.    *
  309.    * @param {Object} config
  310.    * @returns {AxiosPromise}
  311.    */
  312.   async $_retryRequest(config) {
  313.     try {
  314.       const authUrl = `${API_CONFIG.XXX.HOST}${API_CONFIG.XXX.ENDPOINTS.AUTH.CREATE_TOKEN.path}`;
  315.  
  316.       if (config.url !== authUrl) {
  317.         this.$_setAuthHeader(config);
  318.       }
  319.  
  320.       return await axios(config);
  321.     } catch (error) {
  322.       if (process.env.VUE_APP_CONSOLE_MESSAGES === 'on') {
  323.         console.warn('[Auth] An error occurred while retrying request.', error || '(no data)');
  324.       }
  325.  
  326.       throw error;
  327.     }
  328.   },
  329.  
  330.   /**
  331.    * @desc Set the Authorization header on an outgoing request.
  332.    *
  333.    * @param {Object} config
  334.    */
  335.   $_setAuthHeader(config) {
  336.     const store = storeInstance.get(this);
  337.     const accessToken = store.getters[`${AUTH_NAMESPACE}/${AuthGetterTypes.accessToken}`];
  338.  
  339.     // eslint-disable-next-line no-param-reassign
  340.     config.headers['Authorization'] = `Bearer ${accessToken}`;
  341.   },
  342.  
  343.   /**
  344.    * @desc Store access and refresh tokens in the store.
  345.    *
  346.    * @param {string} accessToken
  347.    * @param {string} refreshToken
  348.    */
  349.   $_storeTokens(accessToken, refreshToken) {
  350.     const store = storeInstance.get(this);
  351.  
  352.     store.dispatch(`${AUTH_NAMESPACE}/${AuthActionTypes.storeTokens}`, {
  353.       accessToken,
  354.       refreshToken,
  355.     });
  356.   },
  357.  
  358.   /**
  359.    * @desc Check if the response is caused by an invalid i.e., outdated token.
  360.    *
  361.    * @param {Object} error
  362.    * @returns {boolean}
  363.    */
  364.   $_isInvalidToken(error) {
  365.     const { response } = error;
  366.  
  367.     const authUrl = `${API_CONFIG.XXX.HOST}${API_CONFIG.XXX.ENDPOINTS.AUTH.CREATE_TOKEN.path}`;
  368.  
  369.     const STATUS_CODE_UNAUTHORIZED = 401;
  370.  
  371.     return get(response, 'config.url') !== authUrl && get(response, 'status') === STATUS_CODE_UNAUTHORIZED;
  372.   },
  373.  
  374.   /**
  375.    * @desc Route guard.
  376.    *
  377.    * @param {Object} to
  378.    * @param {Object} from
  379.    * @param {Function} next
  380.    */
  381.   $_routeCheckAuth(to, from, next) {
  382.     const route = findLast(to.matched, route_ => route_.meta.requiresAuth);
  383.     const requiredRole = get(route, 'meta.requiresAuth');
  384.  
  385.     if (!this.checkRole() && requiredRole) {
  386.       return next({ name: PageNames.SIGN_IN });
  387.     }
  388.  
  389.     if (!this.checkRole(requiredRole)) {
  390.       // eslint-disable-next-line arrow-body-style
  391.       const firstEligibleRoute = inNavRoutes.find((nextRoute) => {
  392.         return !nextRoute.meta.requiresAuth || this.checkRole(nextRoute.meta.requiresAuth);
  393.       });
  394.  
  395.       if (!firstEligibleRoute) {
  396.         return this.signOut();
  397.       }
  398.  
  399.       if (from.name !== firstEligibleRoute.name) {
  400.         return next({ name: firstEligibleRoute.name });
  401.       }
  402.     } else {
  403.       return next();
  404.     }
  405.  
  406.     return false;
  407.   },
  408.  
  409.   /**
  410.    * @desc Method for authentication with password.
  411.    *
  412.    * @param {string} username
  413.    * @param {string} password
  414.    * @returns {Promise}
  415.    */
  416.   $_passwordGrant(username, password) {
  417.     if (!(typeof username === 'string' && !!username.trim()) && !(typeof password === 'string' && !!password.trim())) {
  418.       throw new TypeError('Username and password must be non-empty strings.');
  419.     }
  420.  
  421.     return this.$_createToken('password', { username: username.trim(), password: password.trim() });
  422.   },
  423.  
  424.   /**
  425.    * @desc Method for refreshing expired access tokens.
  426.    *
  427.    * @param {string} refreshToken
  428.    * @returns {Promise}
  429.    */
  430.   $_refreshTokenGrant(refreshToken) {
  431.     if (!(typeof refreshToken === 'string' && !!refreshToken.trim())) {
  432.       throw new TypeError('Refresh token must be non-empty string.');
  433.     }
  434.  
  435.     return this.$_createToken('refresh_token', { refresh_token: refreshToken.trim() });
  436.   },
  437.  
  438.   /**
  439.    * @desc Method for creating access token.
  440.    *
  441.    * @param {string} grantType
  442.    * @param {Object} payload
  443.    * @returns {Promise}
  444.    */
  445.   async $_createToken(grantType, payload) {
  446.     try {
  447.       if (!(typeof grantType === 'string' && !!grantType.trim())) {
  448.         throw new TypeError('Grant type must be non-empty string.');
  449.       }
  450.  
  451.       const { method, path } = API_CONFIG.XXX.ENDPOINTS.AUTH.CREATE_TOKEN;
  452.       const { HOST: host, OPTIONS: options } = API_CONFIG.XXX;
  453.  
  454.       const response = await axios[method](
  455.         `${host}${path}`,
  456.         { grant_type: grantType, ...payload },
  457.         options,
  458.       );
  459.  
  460.       return response.data.data;
  461.     } catch (error) {
  462.       if (process.env.VUE_APP_CONSOLE_MESSAGES === 'on') {
  463.         console.warn(`[Auth] An error occurred while creating token with ${grantType} grant.`, error || '(no data)');
  464.       }
  465.  
  466.       throw error;
  467.     }
  468.   },
  469. };
Add Comment
Please, Sign In to add comment