redirection page 401 pour les user non autorises

This commit is contained in:
kevin 2026-04-28 11:50:51 +03:00
parent 1d18659bd7
commit 34875321ac
7 changed files with 301 additions and 3 deletions

View File

@ -65,6 +65,8 @@
<script> <script>
import SidenavItem from "./SidenavItem.vue"; import SidenavItem from "./SidenavItem.vue";
import SidenavCollapse from "./SidenavCollapse.vue"; import SidenavCollapse from "./SidenavCollapse.vue";
import pinia from "@/plugins/pinia";
import { useAuthStore } from "@/stores/auth";
import Shop from "../../components/Icon/Shop.vue"; import Shop from "../../components/Icon/Shop.vue";
import Office from "../../components/Icon/Office.vue"; import Office from "../../components/Icon/Office.vue";
@ -87,10 +89,13 @@ export default {
}, },
computed: { computed: {
...mapState(["isRTL"]), ...mapState(["isRTL"]),
authStore() {
return useAuthStore(pinia);
},
// Navigation data structure // Navigation data structure
navigationItems() { navigationItems() {
return [ const items = [
{ {
id: "dashboard", id: "dashboard",
type: "collapse", type: "collapse",
@ -410,37 +415,118 @@ export default {
icon: "Office", icon: "Office",
collapseRef: "parametrageMenu", collapseRef: "parametrageMenu",
routeKey: "parametrage", routeKey: "parametrage",
requiredAny: [
{ role: "administrator" },
{ permission: "config.view" },
{ permission: "config.view_roles" },
{ permission: "crud.user" },
],
children: [ children: [
{
id: "utilisateurs",
route: { name: "Gestion utilisateurs" },
miniIcon: "U",
text: "Gestion utilisateurs",
requiredPermission: "crud.user",
allowAdministrator: true,
},
{
id: "utilisateurs-create",
route: { name: "Créer utilisateur" },
miniIcon: "A",
text: "Créer utilisateur",
requiredPermission: "crud.user",
allowAdministrator: true,
},
{ {
id: "droits", id: "droits",
route: { name: "Gestion droits" }, route: { name: "Gestion droits" },
miniIcon: "D", miniIcon: "D",
text: "Gestion des droits", text: "Gestion des droits",
requiredPermission: "config.view_roles",
allowAdministrator: true,
}, },
{ {
id: "emails", id: "emails",
route: { name: "Gestion emails" }, route: { name: "Gestion emails" },
miniIcon: "E", miniIcon: "E",
text: "Gestion des emails", text: "Gestion des emails",
requiredPermission: "config.view",
allowAdministrator: true,
}, },
{ {
id: "modeles", id: "modeles",
route: { name: "Gestion modeles" }, route: { name: "Gestion modeles" },
miniIcon: "M", miniIcon: "M",
text: "Gestion des modèles", text: "Gestion des modèles",
requiredPermission: "config.view",
allowAdministrator: true,
}, },
{ {
id: "categories-produits", id: "categories-produits",
route: { name: "Product Categories" }, route: { name: "Product Categories" },
miniIcon: "C", miniIcon: "C",
text: "Catégories Produits", text: "Catégories Produits",
requiredPermission: "config.view",
allowAdministrator: true,
}, },
], ],
}, },
]; ];
return this.filterNavigationItems(items);
}, },
}, },
methods: { methods: {
canAccessItem(item) {
if (item.allowAdministrator && this.authStore.hasRole("administrator")) {
return true;
}
if (item.requiredPermission) {
return this.authStore.hasPermission(item.requiredPermission);
}
if (Array.isArray(item.requiredAny) && item.requiredAny.length > 0) {
return item.requiredAny.some((access) => {
if (access.role) {
return this.authStore.hasRole(access.role);
}
if (access.permission) {
return this.authStore.hasPermission(access.permission);
}
return false;
});
}
return true;
},
filterNavigationItems(items) {
return items
.map((item) => {
if (item.type !== "collapse") {
return item;
}
const children = (item.children || []).filter((child) =>
this.canAccessItem(child)
);
if (!children.length || !this.canAccessItem(item)) {
return null;
}
return {
...item,
children,
};
})
.filter(Boolean);
},
getRoute() { getRoute() {
const routeArr = this.$route.path.split("/"); const routeArr = this.$route.path.split("/");
return routeArr[1]; return routeArr[1];

View File

@ -48,6 +48,7 @@ import VerificationIllustration from "../views/auth/verification/Illustration.vu
import SignupBasic from "../views/auth/signup/Basic.vue"; import SignupBasic from "../views/auth/signup/Basic.vue";
import SignupCover from "../views/auth/signup/Cover.vue"; import SignupCover from "../views/auth/signup/Cover.vue";
import SignupIllustration from "../views/auth/signup/Illustration.vue"; import SignupIllustration from "../views/auth/signup/Illustration.vue";
import Error401 from "../views/auth/error/Error401.vue";
import Error404 from "../views/auth/error/Error404.vue"; import Error404 from "../views/auth/error/Error404.vue";
import Error500 from "../views/auth/error/Error500.vue"; import Error500 from "../views/auth/error/Error500.vue";
import lockBasic from "../views/auth/lock/Basic.vue"; import lockBasic from "../views/auth/lock/Basic.vue";
@ -353,6 +354,12 @@ const routes = [
component: SignupIllustration, component: SignupIllustration,
meta: { guestLayout: true }, meta: { guestLayout: true },
}, },
{
path: "/authentication/error/error401",
name: "Error Error401",
component: Error401,
meta: { public: true, guestLayout: true },
},
{ {
path: "/authentication/error/error404", path: "/authentication/error/error404",
name: "Error Error404", name: "Error Error404",
@ -782,25 +789,77 @@ const routes = [
component: () => import("@/views/pages/Defunts/DefuntDetails.vue"), component: () => import("@/views/pages/Defunts/DefuntDetails.vue"),
}, },
// Paramétrage // Paramétrage
{
path: "/parametrage",
name: "Paramétrage",
redirect: "/parametrage/droits",
meta: {
requiredPermission: "config.view",
allowAdministrator: true,
},
},
{ {
path: "/parametrage/droits", path: "/parametrage/droits",
name: "Gestion droits", name: "Gestion droits",
component: () => import("@/views/pages/Parametrage/Droits.vue"), component: () => import("@/views/pages/Parametrage/Droits.vue"),
meta: {
requiredPermission: "config.view_roles",
allowAdministrator: true,
},
},
{
path: "/parametrage/utilisateurs",
name: "Gestion utilisateurs",
component: () => import("@/views/pages/Parametrage/Users.vue"),
meta: {
requiredPermission: "crud.user",
allowAdministrator: true,
},
},
{
path: "/parametrage/utilisateurs/creation",
name: "Créer utilisateur",
component: () => import("@/views/pages/Parametrage/UserCreate.vue"),
meta: {
requiredPermission: "crud.user",
allowAdministrator: true,
},
},
{
path: "/parametrage/utilisateurs/:id",
name: "Détail utilisateur",
component: () => import("@/views/pages/Parametrage/UserDetails.vue"),
meta: {
requiredPermission: "crud.user",
allowAdministrator: true,
},
}, },
{ {
path: "/parametrage/emails", path: "/parametrage/emails",
name: "Gestion emails", name: "Gestion emails",
component: () => import("@/views/pages/Parametrage/Emails.vue"), component: () => import("@/views/pages/Parametrage/Emails.vue"),
meta: {
requiredPermission: "config.view",
allowAdministrator: true,
},
}, },
{ {
path: "/parametrage/modeles", path: "/parametrage/modeles",
name: "Gestion modeles", name: "Gestion modeles",
component: () => import("@/views/pages/Parametrage/Modeles.vue"), component: () => import("@/views/pages/Parametrage/Modeles.vue"),
meta: {
requiredPermission: "config.view",
allowAdministrator: true,
},
}, },
{ {
path: "/parametrage/categories-produits", path: "/parametrage/categories-produits",
name: "Product Categories", name: "Product Categories",
component: () => import("@/views/pages/Parametrage/ProductCategories.vue"), component: () => import("@/views/pages/Parametrage/ProductCategories.vue"),
meta: {
requiredPermission: "config.view",
allowAdministrator: true,
},
}, },
]; ];
@ -816,6 +875,32 @@ import pinia from "@/plugins/pinia";
const DASHBOARD = "/dashboards/dashboard-default"; const DASHBOARD = "/dashboards/dashboard-default";
const LOGIN = "/login"; // canonical login path without redirect query const LOGIN = "/login"; // canonical login path without redirect query
const UNAUTHORIZED = "/authentication/error/error401";
const canAccessRoute = (auth, to) => {
const accessMeta = to.matched.reduce(
(carry, record) => ({
requiredPermission:
record.meta?.requiredPermission ?? carry.requiredPermission,
allowAdministrator:
record.meta?.allowAdministrator ?? carry.allowAdministrator,
}),
{
requiredPermission: null,
allowAdministrator: false,
}
);
if (accessMeta.allowAdministrator && auth.hasRole("administrator")) {
return true;
}
if (accessMeta.requiredPermission) {
return auth.hasPermission(accessMeta.requiredPermission);
}
return true;
};
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
const auth = useAuthStore(pinia); const auth = useAuthStore(pinia);
@ -848,6 +933,26 @@ router.beforeEach(async (to, from, next) => {
} }
if (auth.isAuthenticated) { if (auth.isAuthenticated) {
if (to.path === "/parametrage") {
if (auth.hasRole("administrator") || auth.hasPermission("crud.user")) {
return next({ name: "Gestion utilisateurs" });
}
if (auth.hasPermission("config.view_roles")) {
return next({ name: "Gestion droits" });
}
if (auth.hasPermission("config.view")) {
return next({ name: "Gestion emails" });
}
return next({ path: UNAUTHORIZED, replace: true });
}
if (!canAccessRoute(auth, to)) {
return next({ path: UNAUTHORIZED, replace: true });
}
// Prevent authenticated users from visiting guest-only routes (like sign-in) // Prevent authenticated users from visiting guest-only routes (like sign-in)
if (isGuestOnly || isLoginRoute) return next(DASHBOARD); if (isGuestOnly || isLoginRoute) return next(DASHBOARD);
return next(); return next();

View File

@ -19,6 +19,16 @@ export interface User {
email_verified_at: string | null; email_verified_at: string | null;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
roles?: Array<{
id: number;
name: string;
guard_name?: string;
}>;
permissions?: Array<{
id: number;
name: string;
guard_name?: string;
}>;
} }
export interface LoginResponse { export interface LoginResponse {

View File

@ -39,6 +39,7 @@ export interface UpdateUserPayload {
name: string; name: string;
email: string; email: string;
password?: string | null; password?: string | null;
clear_password?: boolean;
roles?: string[]; roles?: string[];
permissions?: string[]; permissions?: string[];
} }

View File

@ -15,8 +15,21 @@ export const useAuthStore = defineStore("auth", {
}), }),
getters: { getters: {
isAuthenticated: (state) => !!state.user && !!state.token, isAuthenticated: (state) => !!state.user && !!state.token,
roleNames: (state) =>
(state.user?.roles || []).map((role) => role.name.toLowerCase()),
permissionNames: (state) =>
(state.user?.permissions || []).map((permission) => permission.name),
}, },
actions: { actions: {
hasRole(role: string) {
return this.roleNames.includes(role.toLowerCase());
},
hasPermission(permission: string) {
return this.permissionNames.includes(permission);
},
hasAnyPermission(permissions: string[] = []) {
return permissions.some((permission) => this.hasPermission(permission));
},
async fetchMe() { async fetchMe() {
try { try {
const userData = await AuthService.me(); const userData = await AuthService.me();

View File

@ -76,7 +76,10 @@ export const useUserStore = defineStore("user", () => {
try { try {
const user = await UserService.createUser(payload); const user = await UserService.createUser(payload);
users.value = [user, ...users.value.filter((item) => item.id !== user.id)]; users.value = [
user,
...users.value.filter((item) => item.id !== user.id),
];
currentUser.value = user; currentUser.value = user;
searchedUser.value = user; searchedUser.value = user;
return user; return user;
@ -95,7 +98,9 @@ export const useUserStore = defineStore("user", () => {
try { try {
const user = await UserService.updateUser(payload); const user = await UserService.updateUser(payload);
users.value = users.value.map((item) => (item.id === user.id ? user : item)); users.value = users.value.map((item) =>
item.id === user.id ? user : item
);
currentUser.value = user; currentUser.value = user;
searchedUser.value = user; searchedUser.value = user;
return user; return user;

View File

@ -0,0 +1,78 @@
<template>
<navbar btn-background="bg-gradient-dark" :dark-mode="true" />
<main class="main-content mt-0 ps">
<section class="my-10">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-6 my-auto">
<h1 class="display-1 text-bolder text-gradient text-warning">
401
</h1>
<h2>Accès non autorisé</h2>
<p class="lead">
Vous n'avez pas les droits nécessaires pour accéder à cette page.
</p>
<router-link to="/dashboards/dashboard-default">
<soft-button color="dark" variant="gradient" class="mt-4">
Retour au dashboard
</soft-button>
</router-link>
</div>
<div class="col-lg-6 my-auto position-relative text-center">
<div class="error-401-visual mx-auto">
<span>401</span>
</div>
</div>
</div>
</div>
</section>
</main>
<app-footer />
</template>
<script>
import Navbar from "@/examples/PageLayout/Navbar.vue";
import AppFooter from "@/examples/PageLayout/Footer.vue";
import SoftButton from "@/components/SoftButton.vue";
import { mapMutations } from "vuex";
export default {
name: "Error401",
components: {
Navbar,
AppFooter,
SoftButton,
},
created() {
this.toggleEveryDisplay();
this.toggleHideConfig();
},
beforeUnmount() {
this.toggleEveryDisplay();
this.toggleHideConfig();
},
methods: {
...mapMutations(["toggleEveryDisplay", "toggleHideConfig"]),
},
};
</script>
<style scoped>
.error-401-visual {
width: 260px;
height: 260px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f59e0b, #f97316);
box-shadow: 0 1.5rem 3rem rgba(245, 158, 11, 0.25);
}
.error-401-visual span {
color: #fff;
font-size: 4.5rem;
font-weight: 800;
letter-spacing: 0.08em;
}
</style>