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

@@ -9,4 +9,6 @@ KEYCLOAK_SERVER=
KEYCLOAK_REALM=
KEYCLOAK_CLIENT_ID=
KEYCLOAK_CLIENT_SECRET=
KEYCLOAK_REDIRECT_URI=
KEYCLOAK_REDIRECT_URI=
DEBUG=True
MAX_AGE=3600

View File

@@ -1,11 +1,12 @@
from fastapi import APIRouter, Security, HTTPException, Depends, Request
from typing import Annotated
from fastapi import APIRouter, Security, HTTPException, Depends, Request, Cookie
from fastapi.responses import RedirectResponse, Response
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlmodel import Session, select
import jwt
from jwt import PyJWKClient
from src.settings import AUTH_URL, TOKEN_URL, JWKS_URL, ISSUER, settings
from src.settings import AUTH_URL, TOKEN_URL, JWKS_URL, ISSUER, LOGOUT_URL, settings
import src.users.service as service
from src.database import get_session
from src.models import UserCreate, User, UserPublic
@@ -20,11 +21,45 @@ router = APIRouter(prefix='/auth')
jwk_client = PyJWKClient(JWKS_URL)
security = HTTPBearer()
@router.post('/logout')
def logout(response: Response):
response.delete_cookie('access_token')
response.delete_cookie('refresh_token')
return {'detail': messages.userloggedout}
@router.get('/logout')
def logout(
id_token: Annotated[str | None, Cookie()] = None,
refresh_token: Annotated[str | None, Cookie()] = None,
):
if refresh_token:
print("invalidate tokens")
requests.post(LOGOUT_URL, data={
"client_id": settings.keycloak_client_id,
"client_secret": settings.keycloak_client_secret,
"refresh_token": refresh_token
})
if id_token:
print("redirect keycloak")
response = RedirectResponse(f'{LOGOUT_URL}?post_logout_redirect_uri={settings.origins}&id_token_hint={id_token}')
else:
response = RedirectResponse(settings.origins)
print("clear cookies")
response.delete_cookie(
key='access_token',
path='/',
secure=not settings.debug,
samesite='lax',
)
response.delete_cookie(
key='refresh_token',
path='/',
secure=not settings.debug,
samesite='lax',
)
response.delete_cookie(
key='id_token',
path='/',
secure=not settings.debug,
samesite='lax',
)
return response
@router.get('/login')
@@ -64,7 +99,27 @@ def callback(code: str, session: Session = Depends(get_session)):
id_token = token_data['id_token']
decoded_token = jwt.decode(id_token, options={'verify_signature': False})
decoded_access_token = jwt.decode(token_data['access_token'], options={'verify_signature': False})
roles = decoded_access_token['resource_access'][settings.keycloak_client_id]
resource_access = decoded_access_token.get('resource_access')
if not resource_access:
data = {
'client_id': settings.keycloak_client_id,
'client_secret': settings.keycloak_client_secret,
'refresh_token': token_data['refresh_token'],
}
res = requests.post(LOGOUT_URL, data=data)
resp = RedirectResponse(settings.origins)
return resp
resource_access.get(settings.keycloak_client_id)
if not roles:
data = {
'client_id': settings.keycloak_client_id,
'client_secret': settings.keycloak_client_secret,
'refresh_token': token_data['refresh_token'],
}
res = requests.post(LOGOUT_URL, data=data)
resp = RedirectResponse(settings.origins)
return resp
user_create = UserCreate(
email=decoded_token.get('email'),
name=decoded_token.get('preferred_username'),
@@ -76,18 +131,27 @@ def callback(code: str, session: Session = Depends(get_session)):
key='access_token',
value=token_data['access_token'],
httponly=True,
secure=True if settings.debug == False else True,
samesite='strict',
secure=not settings.debug,
samesite='lax',
max_age=settings.max_age
)
response.set_cookie(
key='refresh_token',
value=token_data['refresh_token'] or '',
httponly=True,
secure=True if settings.debug == False else True,
samesite='strict',
secure=not settings.debug,
samesite='lax',
max_age=30 * 24 * settings.max_age
)
response.set_cookie(
key='id_token',
value=token_data['id_token'],
httponly=True,
secure=not settings.debug,
samesite='lax',
max_age=settings.max_age
)
return response
def verify_token(token: str):
@@ -109,12 +173,12 @@ def verify_token(token: str):
def get_current_user(request: Request, session: Session = Depends(get_session)):
access_token = request.cookies.get("access_token")
access_token = request.cookies.get('access_token')
if not access_token:
raise HTTPException(status_code=401, detail=messages.notauthenticated)
payload = verify_token(access_token)
if not payload:
raise HTTPException(status_code=401, detail="aze")
raise HTTPException(status_code=401, detail='aze')
email = payload.get('email')
if not email:
@@ -125,16 +189,55 @@ def get_current_user(request: Request, session: Session = Depends(get_session)):
raise HTTPException(status_code=401, detail=messages.usernotfound)
return user
@router.post('/refresh')
def refresh_token(refresh_token: Annotated[str | None, Cookie()] = None):
refresh = refresh_token
data = {
'grant_type': 'refresh_token',
'client_id': settings.keycloak_client_id,
'client_secret': settings.keycloak_client_secret,
'refresh_token': refresh,
}
headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
result = requests.post(TOKEN_URL, data=data, headers=headers)
if result.status_code != 200:
raise HTTPException(
status_code=400,
detail=messages.failtogettoken
)
token_data = result.json()
response = Response()
response.set_cookie(
key='access_token',
value=token_data['access_token'],
httponly=True,
secure=True if settings.debug == False else True,
samesite='lax',
max_age=settings.max_age
)
response.set_cookie(
key='refresh_token',
value=token_data['refresh_token'] or '',
httponly=True,
secure=True if settings.debug == False else True,
samesite='lax',
max_age=4
)
return response
@router.get('/user/me')
def me(user: UserPublic = Depends(get_current_user)):
if not user:
return {"logged": False}
return {'logged': False}
return {
"logged": True,
"user": {
"name": user.name,
"email": user.email,
"id": user.id,
"roles": [role.name for role in user.roles]
'logged': True,
'user': {
'name': user.name,
'email': user.email,
'id': user.id,
'roles': [role.name for role in user.roles]
}
}

View File

@@ -73,7 +73,6 @@ def create_occasional_dict(contract_products: list[models.ContractProduct]):
async def create_contract(
contract: models.ContractCreate,
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user)
):
new_contract = service.create_one(session, contract)
occasional_contract_products = list(filter(lambda contract_product: contract_product.product.type == models.ProductType.OCCASIONAL, new_contract.products))

View File

@@ -18,7 +18,9 @@ app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=[settings.origins],
allow_origins=[
settings.origins
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],

View File

@@ -5,4 +5,5 @@ invalidtoken = "Invalid token"
notauthenticated = "Not authenticated"
usernotfound = "User not found"
userloggedout = "User logged out"
failtogettoken = "Failed to get token"
failtogettoken = "Failed to get token"
unauthorized = "Unauthorized"

View File

@@ -25,3 +25,4 @@ AUTH_URL = f"{settings.keycloak_server}/realms/{settings.keycloak_realm}/protoco
TOKEN_URL = f"{settings.keycloak_server}/realms/{settings.keycloak_realm}/protocol/openid-connect/token"
ISSUER = f"{settings.keycloak_server}/realms/{settings.keycloak_realm}"
JWKS_URL = f"{ISSUER}/protocol/openid-connect/certs"
LOGOUT_URL = f'{settings.keycloak_server}/realms/{settings.keycloak_realm}/protocol/openid-connect/logout'

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 />;
}