diff --git a/.env.example b/.env.example index ba5add5..fb3c91f 100644 --- a/.env.example +++ b/.env.example @@ -9,4 +9,6 @@ KEYCLOAK_SERVER= KEYCLOAK_REALM= KEYCLOAK_CLIENT_ID= KEYCLOAK_CLIENT_SECRET= -KEYCLOAK_REDIRECT_URI= \ No newline at end of file +KEYCLOAK_REDIRECT_URI= +DEBUG=True +MAX_AGE=3600 \ No newline at end of file diff --git a/backend/src/auth/auth.py b/backend/src/auth/auth.py index 356f3e0..9de40ea 100644 --- a/backend/src/auth/auth.py +++ b/backend/src/auth/auth.py @@ -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] } } \ No newline at end of file diff --git a/backend/src/contracts/contracts.py b/backend/src/contracts/contracts.py index c419626..0ab3b03 100644 --- a/backend/src/contracts/contracts.py +++ b/backend/src/contracts/contracts.py @@ -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)) diff --git a/backend/src/main.py b/backend/src/main.py index 0fd877e..29b48f0 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -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=["*"], diff --git a/backend/src/messages.py b/backend/src/messages.py index ce741dc..af1c12d 100644 --- a/backend/src/messages.py +++ b/backend/src/messages.py @@ -5,4 +5,5 @@ invalidtoken = "Invalid token" notauthenticated = "Not authenticated" usernotfound = "User not found" userloggedout = "User logged out" -failtogettoken = "Failed to get token" \ No newline at end of file +failtogettoken = "Failed to get token" +unauthorized = "Unauthorized" \ No newline at end of file diff --git a/backend/src/settings.py b/backend/src/settings.py index d092e65..22e703d 100644 --- a/backend/src/settings.py +++ b/backend/src/settings.py @@ -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' \ No newline at end of file diff --git a/frontend/src/components/Navbar/index.tsx b/frontend/src/components/Navbar/index.tsx index 65c2770..0ec4bd2 100644 --- a/frontend/src/components/Navbar/index.tsx +++ b/frontend/src/components/Navbar/index.tsx @@ -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 ( @@ -37,15 +37,14 @@ export function Navbar() { {t("login with keycloak", { capfirst: true })} ) : ( - + )} ); diff --git a/frontend/src/components/Shipments/Modal/index.tsx b/frontend/src/components/Shipments/Modal/index.tsx index 91f1cdc..6a9636f 100644 --- a/frontend/src/components/Shipments/Modal/index.tsx +++ b/frontend/src/components/Shipments/Modal/index.tsx @@ -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, diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 810bee9..53b78fb 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -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( - + + + , diff --git a/frontend/src/pages/Dashboard/index.tsx b/frontend/src/pages/Dashboard/index.tsx index adf6b2c..dbdd0af 100644 --- a/frontend/src/pages/Dashboard/index.tsx +++ b/frontend/src/pages/Dashboard/index.tsx @@ -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}`)} > - {t("help", { capfirst: true })} - {t("productors", { capfirst: true })} - {t("products", { capfirst: true })} - {t("forms", { capfirst: true })} - {t("shipments", { capfirst: true })} - {t("contracts", { capfirst: true })} - {t("users", { capfirst: true })} + ()} value="help">{t("help", { capfirst: true })} + ()} value="productors">{t("productors", { capfirst: true })} + ()} value="products">{t("products", { capfirst: true })} + ()} value="forms">{t("forms", { capfirst: true })} + ()} value="shipments">{t("shipments", { capfirst: true })} + ()} value="contracts">{t("contracts", { capfirst: true })} + ()} value="users">{t("users", { capfirst: true })} diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 4740602..b20762f 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -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: , + element: , children: [ { path: "/dashboard", diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 694c75c..35ec175 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -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 { const queryString = filters?.toString(); return useQuery({ 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({ 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({ 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({ 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
({ 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({ 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({ 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({ 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({ 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({ 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({ 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({ 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({ 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"] }); - }, - }); -} +} \ No newline at end of file diff --git a/frontend/src/services/auth/AuthProvider.tsx b/frontend/src/services/auth/AuthProvider.tsx new file mode 100644 index 0000000..397548b --- /dev/null +++ b/frontend/src/services/auth/AuthProvider.tsx @@ -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(undefined) + +export function AuthProvider({ children }: {children: React.ReactNode}) { + const {data: loggedUser, isLoading} = useCurrentUser(); + + const value: Auth = { + loggedUser: loggedUser ?? null, + isLoading, + }; + + return ( + + {children} + + ) +} + +export function useAuth(): Auth { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used inside AuthProvider"); + } + return context; +} \ No newline at end of file diff --git a/frontend/src/components/Auth/index.tsx b/frontend/src/services/auth/ProtectedRoute/index.tsx similarity index 60% rename from frontend/src/components/Auth/index.tsx rename to frontend/src/services/auth/ProtectedRoute/index.tsx index 2a47c84..6eb97f4 100644 --- a/frontend/src/components/Auth/index.tsx +++ b/frontend/src/services/auth/ProtectedRoute/index.tsx @@ -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 ( ); - if (!userLogged?.logged) { + if (!loggedUser?.logged) { return ; }