Авторизация
Начнём работу с настройки взаимодействия с бэкендом: нужно реализовать получение и сохранение JWT, чтобы затем использовать
их для доступа к закрытым эндпоинтам. Для этого напишем хранилище Pinia и обёртку над fetch
:
import { defineStore } from 'pinia';
import { fetchWrapper, router } from '@/helpers';
const baseUrl = `${import.meta.env.VITE_API_URL}`;
export const useAuthStore = defineStore({
id: 'auth',
state: () => ({
access: JSON.parse(localStorage.getItem('access')),
access_expiration: JSON.parse(localStorage.getItem('access_expiration')),
refresh: JSON.parse(localStorage.getItem('refresh')),
refresh_expiration: JSON.parse(localStorage.getItem('refresh_expiration')),
}),
actions: {
async login(code) {
const data = await fetchWrapper.post(`${baseUrl}/oidc/login/`, { code });
this.access = data.access;
this.access_expiration = data.access_expiration;
this.refresh = data.refresh;
this.refresh_expiration = data.refresh_expiration;
localStorage.setItem('access', JSON.stringify(data.access));
localStorage.setItem('access_expiration', JSON.stringify(data.access_expiration));
localStorage.setItem('refresh', JSON.stringify(data.refresh));
localStorage.setItem('refresh_expiration', JSON.stringify(data.refresh_expiration));
await router.push('/survey');
},
async logout() {
this.access = null;
this.access_expiration = null;
this.refresh = null;
this.refresh_expiration = null;
localStorage.removeItem('access');
localStorage.removeItem('access_expiration');
localStorage.removeItem('refresh');
localStorage.removeItem('refresh_expiration');
await router.push('/login');
},
updateAccessToken(access, access_expiration) {
this.access = access;
this.access_expiration = access_expiration;
localStorage.setItem('access', JSON.stringify(access));
localStorage.setItem('access_expiration', JSON.stringify(access_expiration));
}
}
});
Объявим в хранилище четыре переменные: access
, access_expiration
, refresh
, refresh_expiration
. Помимо access
токена, нам нужно хранить refresh токен, чтобы не просить пользователя регулярно авторизовываться. И чтобы понимать,
когда нужно обновить токен, будем хранить их время жизни.
Также объявим несколько "действий", чтобы эти токены получить и сохранить:
В функции login
с помощью нашей обёртки на fetch
отправим POST запрос на эндпоинт авторизации вместе с code
,
полученным при вызове функции, в теле запроса.
Сохраним полученные данные как в самом хранилище Pinia, так и в локальном хранилище пользователя, а затем с помощью
роутера переведём пользователя на маршрут /survey
, где будет находиться наш опрос.
В функции logout
очистим хранилище Pinia и локальное хранилище пользователя, а затем переведём пользователя
на страницу авторизации.
В функции updateAccessToken
обновим значения токенов на основе данных, полученных из аргументов функции.
import { useAuthStore } from '@/stores';
const baseUrl = `${import.meta.env.VITE_API_URL}`;
export const fetchWrapper = {
get: request('GET'),
post: request('POST'),
put: request('PUT'),
delete: request('DELETE')
};
function request(method) {
return async (url, body) => {
const headers = await authHeader(url);
const requestOptions = {
method,
headers,
};
if (body) {
requestOptions.headers['Content-Type'] = 'application/json';
requestOptions.body = JSON.stringify(body);
}
return fetch(url, requestOptions).then(handleResponse);
}
}
async function refreshToken(refresh) {
const authStore = useAuthStore();
const response = await fetch(`${baseUrl}/oidc/token/refresh/`, {
method: 'POST',
body: JSON.stringify({ refresh: refresh }),
headers: { 'Content-Type': 'application/json' },
});
if (!response.ok) {
await authStore.logout();
}
const data = await response.json();
if (data.access) {
authStore.updateAccessToken(data.access, data.access_expiration);
}
return data.access;
}
async function checkAndRefreshToken() {
const authStore = useAuthStore();
const { access, access_expiration, refresh } = authStore;
const expirationDate = new Date(access_expiration);
const now = new Date();
const isTokenExpired = now > expirationDate;
if (isTokenExpired) {
return refreshToken(refresh);
}
return access;
}
async function authHeader(url) {
const authStore = useAuthStore();
const isLoggedIn = !!authStore.access;
const isApiUrl = url.startsWith(import.meta.env.VITE_API_URL);
if (isLoggedIn && isApiUrl) {
const access = await checkAndRefreshToken();
return { Authorization: `Bearer ${access}` };
} else {
return {};
}
}
async function handleResponse(response) {
return response.text().then(async text => {
const data = text && JSON.parse(text);
if (!response.ok) {
const authStore = useAuthStore();
if ([401, 403].includes(response.status) && authStore.access) {
await authStore.logout();
}
const error = (data && data.message) || response.statusText;
return Promise.reject(error);
}
return data;
});
}
Вызывать обёртку будем с помощью словаря, где ключи - названием методов, а значения - функции с одноимённым аргументом.
request
- основная функция. На вход функция получает URL адрес, на который нужно отправить запрос и тело запроса, если
оно необходимо.
В самой функции мы получаем заголовок с JWT токеном с помощью метода authHeader
нашей обёртки, конвертируем тело в JSON,
а затем вызываем оригинальный метод fetch
с этими параметрами. Полученный ответ обрабатываем с помощью функции handleResponse
.
В функции refreshToken
отправляем POST запрос на эндпоинт обновления токена с refresh токеном в теле запроса.
Если статус ответа - не 200, то вызываем функцию logout
нашего хранилища.
В ином случае парсим ответ и вызываем функцию updateAccessToken
хранилища, чтобы обновить токены. Также возвращаем
access токен.
В функции checkAndRefreshToken
получим из хранилища access токен, его время жизни и refresh токен. Проверим, живой ли
ещё access токен, и, если нет, - вызовем ранее созданную функцию refreshToken
. Если же живой, то просто вернём его.
В функции authHeader
проверим, авторизован ли пользователь (проверив наличие access токена) и является ли полученный URL
адресом нашего бэкенда. Если так, то сформируем заголовок и вернём его, в ином случае ничего не вернём.
В функции handleResponse
обработаем полученный ответ: распарсим его и вернём, если полученный статус не входит в
статусы ошибки в авторизации (в таком случае нужно разлогинить пользователя).