Advertisement
Guest User

Auth Example

a guest
Oct 1st, 2018
176
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 11.98 KB | None | 0 0
  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. };
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement