Skip to content

Instantly share code, notes, and snippets.

@web-crab
Last active March 14, 2019 22:28
Show Gist options
  • Save web-crab/5e9dc9c367cfd0f19a32e3849ecc9513 to your computer and use it in GitHub Desktop.
Save web-crab/5e9dc9c367cfd0f19a32e3849ecc9513 to your computer and use it in GitHub Desktop.
/*
Авторизация с токенами access и refresh для Vue.js
Пример использования:
Vue.use(Auth, {
http, // экземпляр Axios
router, // экземпляр VueRouter,
routes: {
login: '/login',
register: '/login?register',
afterLogin: '/dashboard'
},
urls: {
login: 'auth/login',
register: 'auth/register',
refresh: 'auth/refresh'
},
tokenKeys: {
access: 'access_token',
refresh: 'refresh_token'
}
})
// В компонентах
this.$auth.user
this.$auth.isUserLogged
this.$auth.register(user)
this.$auth.logIn(credentials)
this.$auth.logOut()
*/
import jwtDecode from 'jwt-decode'
import { CancelToken, isCancel } from 'axios'
export default class Auth {
user = null
#config = null
#tokenExpireDate = null
constructor (config) {
const accessToken = localStorage.getItem(config.tokenKeys.access)
const refreshToken = localStorage.getItem(config.tokenKeys.refresh)
const currentPath = location.hash.substring(1)
this.#config = config
this.#addRouterGuard()
this.#injectAxiosHooks()
this.#setAuth(accessToken, refreshToken)
// Повторяем переход по роуту при загрузке страницы, чтобы вызвать хук
config.router.push(currentPath)
}
get isUserLogged () {
const { tokenKeys } = this.#config
return Boolean(this.user && localStorage.getItem(tokenKeys.access))
}
register (user, requestOptions) {
const { http, router, routes, urls, tokenKeys } = this.#config
const redirectTo = router.history.current.query.redirect || routes.afterLogin
return http.post(urls.register, user, requestOptions).then(({ data }) => {
this.#setAuth(data[tokenKeys.access], data[tokenKeys.refresh])
router.push(redirectTo)
})
}
logIn (credentials, requestOptions) {
const { http, router, routes, urls, tokenKeys } = this.#config
const redirectTo = router.history.current.query.redirect || routes.afterLogin
return http.post(urls.login, credentials, requestOptions).then(({ data }) => {
this.#setAuth(data[tokenKeys.access], data[tokenKeys.refresh])
router.push(redirectTo)
})
}
logOut () {
const { router, routes } = this.#config
this.#setAuth(null, null)
router.push(routes.login)
return Promise.resolve()
}
#addRouterGuard () {
const { router, routes, tokenKeys } = this.#config
router.beforeEach((to, from, next) => {
const isPrivateRoute = to.matched.some(route => !route.meta.public)
const isRegisterRoute = to.path === route.register
const isLoginRoute = to.path === routes.login
if (this.isUserLogged && (isLoginRoute || isRegisterRoute)) {
return next(routes.afterLogin)
}
if (!this.isUserLogged && isPrivateRoute) {
return next({
path: routes.login,
query: { redirect: to.fullPath }
})
}
if (!this.isUserLogged && isRegisterRoute) {
return next({
path: routes.register,
query: { redirect: from.fullPath }
})
}
return next()
})
}
#injectAxiosHooks () {
const { http, urls } = this.#config
// Хук перед запросом
http.interceptors.request.use((request) => {
const isTokenExpired = this.#expireDate && (this.#expireDate < Date.now())
const isRefreshRequest = request.url.indexOf(urls.refresh) !== -1
// Отмена запроса, если токен истек
if (isTokenExpired && !isRefreshRequest) {
request.__NEED_REFRESH__ = true
request.cancelToken = new CancelToken(c => c(request))
}
return request
})
// Хук ошибочного ответа
http.interceptors.response.use(null, (err) => {
const isCancelByExpire = isCancel(err) && err.message && err.message.__NEED_REFRESH__
const isAuthError = err.response && err.response.status === 401
const isRefreshRequest = err.config && err.config.url.indexOf(urls.refresh) !== -1
const requestConfig = isCancelByExpire ? err.message : err.config
if (isRefreshRequest) {
this.logOut()
} else if (isCancelByExpire || isAuthError) {
// Сперва обновляем токен, затем повторяем запрос
requestConfig.cancelToken = undefined
return this.#refreshToken().then(() => http(requestConfig))
}
return Promise.reject(err)
})
}
#setAuth (accessToken, refreshToken) {
const { http, tokenKeys } = this.#config
if (accessToken && refreshToken) {
const { user, expireDate } = jwtDecode(accessToken)
localStorage.setItem(tokenKeys.access, accessToken)
localStorage.setItem(tokenKeys.refresh, refreshToken)
http.defaults.headers.common.Authorization = `Bearer ${accessToken}`
this.user = user
this.#expireDate = expireDate
} else if (this.user) {
localStorage.removeItem(tokenKeys.access)
localStorage.removeItem(tokenKeys.refresh)
delete http.defaults.headers.common.Authorization
this.user = null
this.#expireDate = null
}
}
#refreshToken () {
if (!this.#refreshToken.refreshPromise) {
const { http, urls, tokenKeys } = this.#config
const refreshToken = localStorage.getItem(tokenKeys.refresh)
this.#refreshToken.refreshPromise = http.post(urls.refresh, { refreshToken })
.then(({ data }) => {
this.#setAuth(data[tokenKeys.access], data[tokenKeys.refresh])
})
.finally(() => {
this.#refreshToken.refreshPromise = null
})
}
return this.#refreshToken.refreshPromise
}
static install (Vue, config) {
Vue.prototype.$auth = new Auth(config)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment