Skip to content

Autentykacja i uprawnienia ​

Logowanie ​

Backend wykorzystuje Laravel Sanctum w trybie SPA, opartym na ciasteczku sesyjnym i tokenie XSRF, a dodatkowo zwraca token Bearer. Instancja axiosa skonfigurowana jest pod oba mechanizmy:

js
// src/modules/core/services/http.js
this.instance = axios.create({
  baseURL: process.env.API_BASE_URL,
  withCredentials: true,
  withXSRFToken: true,
});

Sam przebieg logowania wygląda następująco:

js
// src/modules/auth/stores/auth.js
async login(form) {
  await api.get("sanctum/csrf-cookie");        // 1. pobranie ciasteczka XSRF
  const { data } = await api.post("auth/login", form);

  this.setWasLoggedIn(true);                   // 2. flaga w LocalStorage
  this.setToken(data.token);                   // 3. token → LocalStorage + nagłówek

  if (form.remember_me) {
    this.setLastEmail(form.email);
  }

  return data;
}

Rola flagi wasLoggedIn

Guard nawigacji korzysta z niej po to, aby nie wysyłać żądania do auth/user dla użytkownika, który nigdy się nie logował. Bez tej flagi każde wejście na chronioną trasę kończyłoby się zbędnym żądaniem zakończonym błędem 401.

Store auth ​

Store dostępny jest przez useAuthStore() z pliku modules/auth/stores/auth.js.

Stan ​

PolePersystencjaOpis
tokenLocalStorageToken Bearer
lastEmailLocalStorageZapamiętany adres e-mail przy opcji „zapamiętaj mnie"
wasLoggedInLocalStorageInformacja, czy użytkownik kiedykolwiek się zalogował
currentUser—Obiekt { email, roles, permissions } pochodzący z GET auth/user

Akcje ​

AkcjaWywoływany endpoint
login(form)GET sanctum/csrf-cookie, następnie POST auth/login
logout()GET auth/logout; token i currentUser czyszczone są zawsze, także po nieudanym żądaniu
fetchCurrentUser()GET auth/user
initResetPassword(email)POST auth/reset-password
finishResetPassword(token, { password, password_confirmation })POST auth/reset-password/store
updatePassword({ current_password, password, password_confirmation })POST auth/update-password
setToken, setLastEmail, setWasLoggedIn— (zapis do LocalStorage)

Sprawdzanie uprawnień ​

Uprawnienia przychodzą w polu currentUser.permissions jako obiekt, odczytywany przy użyciu lodashowego get, dzięki czemu klucze z kropkami obsługiwane są naturalnie.

GetterArgumentZwraca wartość true, gdy
can(x)łańcuch znaków lub tablicawszystkie podane uprawnienia zostały nadane
canAny(x)łańcuch znaków lub tablicanadane zostało przynajmniej jedno
inRole(slug)łańcuch znakówużytkownik posiada rolę o podanym slugu
middleware(x)opisany poniżejuniwersalny, wykorzystywany przez guard i menu

Getter middleware przyjmuje cztery formy argumentu:

js
// dowolny komponent lub guard
const authStore = useAuthStore();

authStore.middleware("blog.articles.index");                    // → can()
authStore.middleware(["blog.articles.index", "blog.articles.store"]);  // → can(), wszystkie
authStore.middleware({ name: "canAny", permission: ["a.b", "c.d"] });  // → wybrany getter
authStore.middleware(({ can, canAny }) => can("a.b") && canAny(["c.d", "e.f"]));

Zastosowanie w komponencie ​

Dyrektywa v-can ani komponent Can nie występują w projekcie

Uprawnienia sprawdzane są bezpośrednio w bloku <script setup>:

vue
<script setup>
import { useAuthStore } from "modules/auth/stores/auth";

const authStore = useAuthStore();
</script>

<template>
  <base-btn
    v-if="authStore.can('blog.articles.store')"
    :label="$t('buttons.create')"
    :to="{ name: 'blog.articles.create' }"
  />
</template>

Przykładem jest prefetch dysku mediów, wykonywany wyłącznie wtedy, gdy użytkownik ma do niego dostęp:

js
// src/layouts/MasterLayout.vue
if (authStore.can("media.disks.index")) {
  await initDisks();
}

Guard nawigacji ​

Guard rejestrowany jest jednorazowo, na poziomie globalnym:

js
// src/router/index.js
Router.beforeEach(async (to, from, next) => {
  const route = await handleNavigationGuards(to, from);

  if (route === true) return next();

  next(route);   // false = zablokuj nawigację, obiekt = przekieruj
});

Logika zaimplementowana w src/modules/core/helpers/navigation-guards.js przebiega w następujących krokach:

  1. Żaden dopasowany rekord nie ma meta.secured → nawigacja przechodzi.

  2. wasLoggedIn ma wartość false → przekierowanie na auth.login.

  3. await authStore.fetchCurrentUser() — przy każdej nawigacji.

  4. authStore.middleware(to.meta.access ?? to.name) rozstrzyga o dostępie:

    SytuacjaEfekt
    Uprawnienie nadaneNawigacja przechodzi
    Brak uprawnienia, from.name istnieje (przejście z innego ekranu)Toast errors.unauthorized i false — użytkownik zostaje tam, gdzie był
    Brak uprawnienia, from.name nie istnieje (wejście bezpośrednie)Przekierowanie na core.404
  5. Błąd 401 albo 403 przy jednoczesnym braku currentUser.email → przekierowanie na auth.login.

Wywołanie fetchCurrentUser() następuje przy każdej nawigacji

Rozwiązanie celowe: uprawnienia pozostają aktualne również po zmianie roli w innej karcie przeglądarki. Kosztem jest jedno żądanie GET auth/user na każde przejście między ekranami.

Wylogowanie automatyczne ​

Interceptor odpowiedzi w klasie Http wywołuje logout() po otrzymaniu statusu 401:

js
// src/modules/core/services/http.js
if (
  err.response?.status === 401 &&
  err.response?.config?.url !== "auth/logout" &&
  err.config &&
  !err.config.__isRetryRequest
) {
  await useAuthStore().logout();
}

Wyjątek dla adresu auth/logout zapobiega powstaniu pętli w sytuacji, gdy samo wylogowanie zwróci błąd 401.

Ukrywanie elementów menu ​

Każda pozycja i podpozycja w pliku src/menu-items.js posiada pole access:

js
// src/menu-items.js
{
  icon: "article",
  name: t("menu-items.blog"),
  to: { name: "blog.articles.index" },
  access: "blog.articles.index",
  children: [
    {
      name: t("blog.articles.title.index"),
      to: { name: "blog.articles.index" },
      access: "blog.articles.index",
    },
    // ...
  ],
},

Komponent NavigationDrawerItem.vue filtruje pozycje na dwóch poziomach:

js
// NavigationDrawerItem.vue
function can(access) {
  if (isUndefined(access)) return true;   // brak access = zawsze widoczne
  return authStore.middleware(access);
}

const hasChildren = computed(() => {
  const children = props.item.children;
  return children && children.length && children.some((child) => authStore.can(child.access));
});

W rezultacie pozycja bez wymaganego uprawnienia nie jest renderowana, możliwość rozwinięcia znika, gdy żadne dziecko nie jest dostępne, a poszczególne dzieci filtrowane są pojedynczo.

Ukrycie pozycji w menu nie stanowi zabezpieczenia

Menu odpowiada wyłącznie za wygodę korzystania z panelu. Rzeczywistą kontrolę dostępu zapewnia meta.access na trasie w połączeniu z walidacją po stronie backendu. Oba mechanizmy powinny zostać skonfigurowane.

Lista kontrolna dla nowego ekranu z uprawnieniami ​

  1. Ustaw meta.access na trasie dziecka — opisane w Routingu.
  2. Dodaj access w src/menu-items.js, jeżeli ekran ma pozycję w menu.
  3. Dodaj v-if="authStore.can(...)" na przyciskach akcji niedostępnych dla użytkownika.
  4. Zdefiniuj i sprawdź uprawnienie po stronie backendu — punkty 1–3 dotyczą wyłącznie wygody, nie bezpieczeństwa.

Made by Moonbite