add login / logout logic for user

This commit is contained in:
Julien Aldon
2026-02-17 17:31:29 +01:00
parent a8c8c489da
commit aca24ca560
14 changed files with 258 additions and 108 deletions

View File

@@ -1,13 +1,13 @@
import { NavLink } from "react-router";
import { t } from "@/config/i18n";
import "./index.css";
import { Button, Group, Loader } from "@mantine/core";
import { Group, Loader } from "@mantine/core";
import { Config } from "@/config/config";
import { useCurrentUser, useLogoutUser } from "@/services/api";
import { useAuth } from "@/services/auth/AuthProvider";
export function Navbar() {
const { data: user, isLoading } = useCurrentUser();
const logout = useLogoutUser();
const { loggedUser: user, isLoading } = useAuth();
if (!user && isLoading) {
return (
<Group align="center" justify="center" h="80vh" w="100%">
@@ -37,15 +37,14 @@ export function Navbar() {
{t("login with keycloak", { capfirst: true })}
</NavLink>
) : (
<Button
<a
href={`${Config.backend_uri}/auth/logout`}
className={"navLink"}
aria-label={t("logout", { capfirst: true })}
onClick={() => {
logout.mutate();
}}
>
{t("logout", { capfirst: true })}
</Button>
</a>
)}
</nav>
);

View File

@@ -55,7 +55,7 @@ export default function ShipmentModal({
}, [allForms]);
const productsSelect = useMemo(() => {
if (!allProducts) return;
if (!allProducts || !allProductors) return;
return allProductors?.map((productor) => {
return {
group: productor.name,

View File

@@ -8,15 +8,18 @@ import { Notifications } from "@mantine/notifications";
import "@mantine/core/styles.css";
import "@mantine/dates/styles.css";
import "@mantine/notifications/styles.css";
import { AuthProvider } from "./services/auth/AuthProvider";
const queryClient = new QueryClient();
export const queryClient = new QueryClient();
createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<MantineProvider>
<Notifications />
<RouterProvider router={router} />
<AuthProvider>
<RouterProvider router={router} />
</AuthProvider>
</MantineProvider>
</QueryClientProvider>
</StrictMode>,

View File

@@ -1,6 +1,6 @@
import { Tabs } from "@mantine/core";
import { t } from "@/config/i18n";
import { Outlet, useLocation, useNavigate } from "react-router";
import { Link, Outlet, useLocation, useNavigate } from "react-router";
export default function Dashboard() {
const navigate = useNavigate();
@@ -15,13 +15,13 @@ export default function Dashboard() {
onChange={(value) => navigate(`/dashboard/${value}`)}
>
<Tabs.List mb="md">
<Tabs.Tab value="help">{t("help", { capfirst: true })}</Tabs.Tab>
<Tabs.Tab value="productors">{t("productors", { capfirst: true })}</Tabs.Tab>
<Tabs.Tab value="products">{t("products", { capfirst: true })}</Tabs.Tab>
<Tabs.Tab value="forms">{t("forms", { capfirst: true })}</Tabs.Tab>
<Tabs.Tab value="shipments">{t("shipments", { capfirst: true })}</Tabs.Tab>
<Tabs.Tab value="contracts">{t("contracts", { capfirst: true })}</Tabs.Tab>
<Tabs.Tab value="users">{t("users", { capfirst: true })}</Tabs.Tab>
<Tabs.Tab renderRoot={(props) => (<Link to="/dashboard/help" {...props}></Link>)} value="help">{t("help", { capfirst: true })}</Tabs.Tab>
<Tabs.Tab renderRoot={(props) => (<Link to="/dashboard/productors" {...props}></Link>)} value="productors">{t("productors", { capfirst: true })}</Tabs.Tab>
<Tabs.Tab renderRoot={(props) => (<Link to="/dashboard/products" {...props}></Link>)} value="products">{t("products", { capfirst: true })}</Tabs.Tab>
<Tabs.Tab renderRoot={(props) => (<Link to="/dashboard/forms" {...props}></Link>)} value="forms">{t("forms", { capfirst: true })}</Tabs.Tab>
<Tabs.Tab renderRoot={(props) => (<Link to="/dashboard/shipments" {...props}></Link>)} value="shipments">{t("shipments", { capfirst: true })}</Tabs.Tab>
<Tabs.Tab renderRoot={(props) => (<Link to="/dashboard/contracts" {...props}></Link>)} value="contracts">{t("contracts", { capfirst: true })}</Tabs.Tab>
<Tabs.Tab renderRoot={(props) => (<Link to="/dashboard/users" {...props}></Link>)} value="users">{t("users", { capfirst: true })}</Tabs.Tab>
</Tabs.List>
<Outlet />
</Tabs>

View File

@@ -13,7 +13,7 @@ import { NotFound } from "./pages/NotFound";
import Contracts from "./pages/Contracts";
import { Help } from "./pages/Help";
import { Login } from "./pages/Login";
import { Auth } from "./components/Auth";
import { ProtectedRoute } from "./services/auth/ProtectedRoute";
export const router = createBrowserRouter([
{
@@ -24,7 +24,7 @@ export const router = createBrowserRouter([
{ index: true, Component: Home },
{ path: "/forms", Component: Forms },
{
element: <Auth />,
element: <ProtectedRoute />,
children: [
{
path: "/dashboard",

View File

@@ -25,12 +25,40 @@ import type { Contract, ContractCreate } from "./resources/contracts";
import { notifications } from "@mantine/notifications";
import { t } from "@/config/i18n";
export async function refreshToken() {
return await fetch(`${Config.backend_uri}/auth/refresh`, {method: "POST", credentials: "include"});
}
export async function fetchWithAuth(input: RequestInfo, options?: RequestInit) {
const res = await fetch(input, {
credentials: "include",
...options,
});
if (res.status === 401) {
const refresh = await refreshToken();
if (refresh.status == 400 || refresh.status == 401) {
window.location.href = `${Config.backend_uri}/auth/logout`;
const error = new Error("Unauthorized");
error.cause = 401
throw error;
}
const newRes = await fetch(input, {
credentials: "include",
...options,
});
return newRes;
}
return res;
}
export function useGetShipments(filters?: URLSearchParams): UseQueryResult<Shipment[], Error> {
const queryString = filters?.toString();
return useQuery<Shipment[]>({
queryKey: ["shipments", queryString],
queryFn: () =>
fetch(`${Config.backend_uri}/shipments${filters ? `?${queryString}` : ""}`, {
fetchWithAuth(`${Config.backend_uri}/shipments${filters ? `?${queryString}` : ""}`, {
credentials: "include",
}).then((res) => res.json()),
});
@@ -43,7 +71,7 @@ export function useGetShipment(
return useQuery<Shipment>({
queryKey: ["shipment"],
queryFn: () =>
fetch(`${Config.backend_uri}/shipments/${id}`, {
fetchWithAuth(`${Config.backend_uri}/shipments/${id}`, {
credentials: "include",
}).then((res) => res.json()),
enabled: !!id,
@@ -56,7 +84,7 @@ export function useCreateShipment() {
return useMutation({
mutationFn: (newShipment: ShipmentCreate) => {
return fetch(`${Config.backend_uri}/shipments`, {
return fetchWithAuth(`${Config.backend_uri}/shipments`, {
method: "POST",
credentials: "include",
headers: {
@@ -89,7 +117,7 @@ export function useEditShipment() {
return useMutation({
mutationFn: ({ shipment, id }: ShipmentEditPayload) => {
return fetch(`${Config.backend_uri}/shipments/${id}`, {
return fetchWithAuth(`${Config.backend_uri}/shipments/${id}`, {
method: "PUT",
credentials: "include",
headers: {
@@ -121,7 +149,7 @@ export function useDeleteShipment() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/shipments/${id}`, {
return fetchWithAuth(`${Config.backend_uri}/shipments/${id}`, {
method: "DELETE",
credentials: "include",
headers: {
@@ -153,9 +181,11 @@ export function useGetProductors(filters?: URLSearchParams): UseQueryResult<Prod
return useQuery<Productor[]>({
queryKey: ["productors", queryString],
queryFn: () =>
fetch(`${Config.backend_uri}/productors${filters ? `?${queryString}` : ""}`, {
fetchWithAuth(`${Config.backend_uri}/productors${filters ? `?${queryString}` : ""}`, {
credentials: "include",
}).then((res) => res.json()),
})
.then((res) => res.json()
),
});
}
@@ -166,7 +196,7 @@ export function useGetProductor(
return useQuery<Productor>({
queryKey: ["productor"],
queryFn: () =>
fetch(`${Config.backend_uri}/productors/${id}`, {
fetchWithAuth(`${Config.backend_uri}/productors/${id}`, {
credentials: "include",
}).then((res) => res.json()),
enabled: !!id,
@@ -179,7 +209,7 @@ export function useCreateProductor() {
return useMutation({
mutationFn: (newProductor: ProductorCreate) => {
return fetch(`${Config.backend_uri}/productors`, {
return fetchWithAuth(`${Config.backend_uri}/productors`, {
method: "POST",
credentials: "include",
headers: {
@@ -212,7 +242,7 @@ export function useEditProductor() {
return useMutation({
mutationFn: ({ productor, id }: ProductorEditPayload) => {
return fetch(`${Config.backend_uri}/productors/${id}`, {
return fetchWithAuth(`${Config.backend_uri}/productors/${id}`, {
method: "PUT",
credentials: "include",
headers: {
@@ -244,7 +274,7 @@ export function useDeleteProductor() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/productors/${id}`, {
return fetchWithAuth(`${Config.backend_uri}/productors/${id}`, {
method: "DELETE",
credentials: "include",
headers: {
@@ -278,7 +308,7 @@ export function useGetForm(
return useQuery<Form>({
queryKey: ["form"],
queryFn: () =>
fetch(`${Config.backend_uri}/forms/${id}`, {
fetchWithAuth(`${Config.backend_uri}/forms/${id}`, {
credentials: "include",
}).then((res) => res.json()),
enabled: !!id,
@@ -291,9 +321,7 @@ export function useGetForms(filters?: URLSearchParams): UseQueryResult<Form[], E
return useQuery<Form[]>({
queryKey: ["forms", queryString],
queryFn: () =>
fetch(`${Config.backend_uri}/forms${filters ? `?${queryString}` : ""}`, {
credentials: "include",
}).then((res) => res.json()),
fetch(`${Config.backend_uri}/forms${filters ? `?${queryString}` : ""}`).then((res) => res.json()),
});
}
@@ -302,7 +330,7 @@ export function useCreateForm() {
return useMutation({
mutationFn: (newForm: FormCreate) => {
return fetch(`${Config.backend_uri}/forms`, {
return fetchWithAuth(`${Config.backend_uri}/forms`, {
method: "POST",
credentials: "include",
headers: {
@@ -321,7 +349,7 @@ export function useDeleteForm() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/forms/${id}`, {
return fetchWithAuth(`${Config.backend_uri}/forms/${id}`, {
method: "DELETE",
credentials: "include",
headers: {
@@ -353,7 +381,7 @@ export function useEditForm() {
return useMutation({
mutationFn: ({ id, form }: FormEditPayload) => {
return fetch(`${Config.backend_uri}/forms/${id}`, {
return fetchWithAuth(`${Config.backend_uri}/forms/${id}`, {
method: "PUT",
credentials: "include",
headers: {
@@ -387,7 +415,7 @@ export function useGetProduct(
return useQuery<Product>({
queryKey: ["product"],
queryFn: () =>
fetch(`${Config.backend_uri}/products/${id}`, {
fetchWithAuth(`${Config.backend_uri}/products/${id}`, {
credentials: "include",
}).then((res) => res.json()),
enabled: !!id,
@@ -400,7 +428,7 @@ export function useGetProducts(filters?: URLSearchParams): UseQueryResult<Produc
return useQuery<Product[]>({
queryKey: ["products", queryString],
queryFn: () =>
fetch(`${Config.backend_uri}/products${filters ? `?${queryString}` : ""}`, {
fetchWithAuth(`${Config.backend_uri}/products${filters ? `?${queryString}` : ""}`, {
credentials: "include",
}).then((res) => res.json()),
});
@@ -411,7 +439,7 @@ export function useCreateProduct() {
return useMutation({
mutationFn: (newProduct: ProductCreate) => {
return fetch(`${Config.backend_uri}/products`, {
return fetchWithAuth(`${Config.backend_uri}/products`, {
method: "POST",
credentials: "include",
headers: {
@@ -443,7 +471,7 @@ export function useDeleteProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/products/${id}`, {
return fetchWithAuth(`${Config.backend_uri}/products/${id}`, {
method: "DELETE",
credentials: "include",
headers: {
@@ -475,7 +503,7 @@ export function useEditProduct() {
return useMutation({
mutationFn: ({ id, product }: ProductEditPayload) => {
return fetch(`${Config.backend_uri}/products/${id}`, {
return fetchWithAuth(`${Config.backend_uri}/products/${id}`, {
method: "PUT",
credentials: "include",
headers: {
@@ -510,7 +538,7 @@ export function useGetUser(
return useQuery<User>({
queryKey: ["user"],
queryFn: () =>
fetch(`${Config.backend_uri}/users/${id}`, {
fetchWithAuth(`${Config.backend_uri}/users/${id}`, {
credentials: "include",
}).then((res) => res.json()),
enabled: !!id,
@@ -523,7 +551,7 @@ export function useGetUsers(filters?: URLSearchParams): UseQueryResult<User[], E
return useQuery<User[]>({
queryKey: ["users", queryString],
queryFn: () =>
fetch(`${Config.backend_uri}/users${filters ? `?${queryString}` : ""}`, {
fetchWithAuth(`${Config.backend_uri}/users${filters ? `?${queryString}` : ""}`, {
credentials: "include",
}).then((res) => res.json()),
});
@@ -534,7 +562,7 @@ export function useCreateUser() {
return useMutation({
mutationFn: (newUser: UserCreate) => {
return fetch(`${Config.backend_uri}/users`, {
return fetchWithAuth(`${Config.backend_uri}/users`, {
method: "POST",
credentials: "include",
headers: {
@@ -566,7 +594,7 @@ export function useDeleteUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/users/${id}`, {
return fetchWithAuth(`${Config.backend_uri}/users/${id}`, {
method: "DELETE",
credentials: "include",
headers: {
@@ -598,7 +626,7 @@ export function useEditUser() {
return useMutation({
mutationFn: ({ id, user }: UserEditPayload) => {
return fetch(`${Config.backend_uri}/users/${id}`, {
return fetchWithAuth(`${Config.backend_uri}/users/${id}`, {
method: "PUT",
credentials: "include",
headers: {
@@ -630,7 +658,7 @@ export function useGetContracts(filters?: URLSearchParams): UseQueryResult<Contr
return useQuery<Contract[]>({
queryKey: ["contracts", queryString],
queryFn: () =>
fetch(`${Config.backend_uri}/contracts${filters ? `?${queryString}` : ""}`, {
fetchWithAuth(`${Config.backend_uri}/contracts${filters ? `?${queryString}` : ""}`, {
credentials: "include",
}).then((res) => res.json()),
});
@@ -643,7 +671,7 @@ export function useGetContract(
return useQuery<Contract>({
queryKey: ["contract"],
queryFn: () =>
fetch(`${Config.backend_uri}/contracts/${id}`, {
fetchWithAuth(`${Config.backend_uri}/contracts/${id}`, {
credentials: "include",
}).then((res) => res.json()),
enabled: !!id,
@@ -654,7 +682,7 @@ export function useGetContract(
export function useGetContractFile() {
return useMutation({
mutationFn: async (id: number) => {
const res = await fetch(`${Config.backend_uri}/contracts/${id}/file`, {
const res = await fetchWithAuth(`${Config.backend_uri}/contracts/${id}/file`, {
credentials: "include",
}).then((res) => res);
@@ -682,7 +710,7 @@ export function useGetContractFile() {
export function useGetAllContractFile() {
return useMutation({
mutationFn: async (form_id: number) => {
const res = await fetch(`${Config.backend_uri}/contracts/${form_id}/files`, {
const res = await fetchWithAuth(`${Config.backend_uri}/contracts/${form_id}/files`, {
credentials: "include",
}).then((res) => res);
@@ -714,7 +742,6 @@ export function useCreateContract() {
mutationFn: (newContract: ContractCreate) => {
return fetch(`${Config.backend_uri}/contracts`, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
@@ -737,7 +764,7 @@ export function useDeleteContract() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/contracts/${id}`, {
return fetchWithAuth(`${Config.backend_uri}/contracts/${id}`, {
method: "DELETE",
credentials: "include",
headers: {
@@ -769,7 +796,7 @@ export function useGetRoles(filters?: URLSearchParams): UseQueryResult<Role[], E
return useQuery<Role[]>({
queryKey: ["roles", queryString],
queryFn: () =>
fetch(`${Config.backend_uri}/users/roles${filters ? `?${queryString}` : ""}`, {
fetchWithAuth(`${Config.backend_uri}/users/roles${filters ? `?${queryString}` : ""}`, {
credentials: "include",
}).then((res) => res.json()),
});
@@ -784,25 +811,5 @@ export function useCurrentUser() {
}).then((res) => res.json());
},
retry: false,
refetchOnWindowFocus: false,
});
}
export function useLogoutUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => {
return fetch(`${Config.backend_uri}/auth/logout`, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
}).then((res) => res.json());
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["currentUser"] });
},
});
}
}

View File

@@ -0,0 +1,33 @@
import { createContext, useContext } from "react";
import { useCurrentUser } from "../api";
import type { UserLogged } from "../resources/users";
export type Auth = {
loggedUser: UserLogged | null;
isLoading: boolean;
}
const AuthContext = createContext<Auth | undefined>(undefined)
export function AuthProvider({ children }: {children: React.ReactNode}) {
const {data: loggedUser, isLoading} = useCurrentUser();
const value: Auth = {
loggedUser: loggedUser ?? null,
isLoading,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
export function useAuth(): Auth {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used inside AuthProvider");
}
return context;
}

View File

@@ -1,18 +1,18 @@
import { useCurrentUser } from "@/services/api";
import { Group, Loader } from "@mantine/core";
import { Navigate, Outlet } from "react-router";
import { useAuth } from "../AuthProvider";
export function Auth() {
const { data: userLogged, isLoading } = useCurrentUser();
export function ProtectedRoute() {
const { loggedUser, isLoading } = useAuth();
if (!userLogged && isLoading)
if (!loggedUser && isLoading)
return (
<Group align="center" justify="center" h="80vh" w="100%">
<Loader color="pink" />
</Group>
);
if (!userLogged?.logged) {
if (!loggedUser?.logged) {
return <Navigate to="/" replace />;
}