Motyw
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
| Pole | Persystencja | Opis |
|---|---|---|
token | LocalStorage | Token Bearer |
lastEmail | LocalStorage | Zapamiętany adres e-mail przy opcji „zapamiętaj mnie" |
wasLoggedIn | LocalStorage | Informacja, czy użytkownik kiedykolwiek się zalogował |
currentUser | — | Obiekt { email, roles, permissions } pochodzący z GET auth/user |
Akcje
| Akcja | Wywoł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.
| Getter | Argument | Zwraca wartość true, gdy |
|---|---|---|
can(x) | łańcuch znaków lub tablica | wszystkie podane uprawnienia zostały nadane |
canAny(x) | łańcuch znaków lub tablica | nadane zostało przynajmniej jedno |
inRole(slug) | łańcuch znaków | użytkownik posiada rolę o podanym slugu |
middleware(x) | opisany poniżej | uniwersalny, 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:
Żaden dopasowany rekord nie ma
meta.secured→ nawigacja przechodzi.wasLoggedInma wartośćfalse→ przekierowanie naauth.login.await authStore.fetchCurrentUser()— przy każdej nawigacji.authStore.middleware(to.meta.access ?? to.name)rozstrzyga o dostępie:Sytuacja Efekt Uprawnienie nadane Nawigacja przechodzi Brak uprawnienia, from.nameistnieje (przejście z innego ekranu)Toast errors.unauthorizedifalse— użytkownik zostaje tam, gdzie byłBrak uprawnienia, from.namenie istnieje (wejście bezpośrednie)Przekierowanie na core.404Błąd
401albo403przy jednoczesnym brakucurrentUser.email→ przekierowanie naauth.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
- Ustaw
meta.accessna trasie dziecka — opisane w Routingu. - Dodaj
accesswsrc/menu-items.js, jeżeli ekran ma pozycję w menu. - Dodaj
v-if="authStore.can(...)"na przyciskach akcji niedostępnych dla użytkownika. - Zdefiniuj i sprawdź uprawnienie po stronie backendu — punkty 1–3 dotyczą wyłącznie wygody, nie bezpieczeństwa.