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

@@ -10,3 +10,5 @@ KEYCLOAK_REALM=
KEYCLOAK_CLIENT_ID= KEYCLOAK_CLIENT_ID=
KEYCLOAK_CLIENT_SECRET= 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.responses import RedirectResponse, Response
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlmodel import Session, select from sqlmodel import Session, select
import jwt import jwt
from jwt import PyJWKClient 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 import src.users.service as service
from src.database import get_session from src.database import get_session
from src.models import UserCreate, User, UserPublic from src.models import UserCreate, User, UserPublic
@@ -20,11 +21,45 @@ router = APIRouter(prefix='/auth')
jwk_client = PyJWKClient(JWKS_URL) jwk_client = PyJWKClient(JWKS_URL)
security = HTTPBearer() security = HTTPBearer()
@router.post('/logout') @router.get('/logout')
def logout(response: Response): def logout(
response.delete_cookie('access_token') id_token: Annotated[str | None, Cookie()] = None,
response.delete_cookie('refresh_token') refresh_token: Annotated[str | None, Cookie()] = None,
return {'detail': messages.userloggedout} ):
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') @router.get('/login')
@@ -64,7 +99,27 @@ def callback(code: str, session: Session = Depends(get_session)):
id_token = token_data['id_token'] id_token = token_data['id_token']
decoded_token = jwt.decode(id_token, options={'verify_signature': False}) decoded_token = jwt.decode(id_token, options={'verify_signature': False})
decoded_access_token = jwt.decode(token_data['access_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( user_create = UserCreate(
email=decoded_token.get('email'), email=decoded_token.get('email'),
name=decoded_token.get('preferred_username'), name=decoded_token.get('preferred_username'),
@@ -76,18 +131,27 @@ def callback(code: str, session: Session = Depends(get_session)):
key='access_token', key='access_token',
value=token_data['access_token'], value=token_data['access_token'],
httponly=True, httponly=True,
secure=True if settings.debug == False else True, secure=not settings.debug,
samesite='strict', samesite='lax',
max_age=settings.max_age max_age=settings.max_age
) )
response.set_cookie( response.set_cookie(
key='refresh_token', key='refresh_token',
value=token_data['refresh_token'] or '', value=token_data['refresh_token'] or '',
httponly=True, httponly=True,
secure=True if settings.debug == False else True, secure=not settings.debug,
samesite='strict', samesite='lax',
max_age=30 * 24 * settings.max_age 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 return response
def verify_token(token: str): 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)): 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: if not access_token:
raise HTTPException(status_code=401, detail=messages.notauthenticated) raise HTTPException(status_code=401, detail=messages.notauthenticated)
payload = verify_token(access_token) payload = verify_token(access_token)
if not payload: if not payload:
raise HTTPException(status_code=401, detail="aze") raise HTTPException(status_code=401, detail='aze')
email = payload.get('email') email = payload.get('email')
if not 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) raise HTTPException(status_code=401, detail=messages.usernotfound)
return user 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') @router.get('/user/me')
def me(user: UserPublic = Depends(get_current_user)): def me(user: UserPublic = Depends(get_current_user)):
if not user: if not user:
return {"logged": False} return {'logged': False}
return { return {
"logged": True, 'logged': True,
"user": { 'user': {
"name": user.name, 'name': user.name,
"email": user.email, 'email': user.email,
"id": user.id, 'id': user.id,
"roles": [role.name for role in user.roles] '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( async def create_contract(
contract: models.ContractCreate, contract: models.ContractCreate,
session: Session = Depends(get_session), session: Session = Depends(get_session),
user: models.User = Depends(get_current_user)
): ):
new_contract = service.create_one(session, contract) 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)) 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( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=[settings.origins], allow_origins=[
settings.origins
],
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],

View File

@@ -6,3 +6,4 @@ notauthenticated = "Not authenticated"
usernotfound = "User not found" usernotfound = "User not found"
userloggedout = "User logged out" 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" TOKEN_URL = f"{settings.keycloak_server}/realms/{settings.keycloak_realm}/protocol/openid-connect/token"
ISSUER = f"{settings.keycloak_server}/realms/{settings.keycloak_realm}" ISSUER = f"{settings.keycloak_server}/realms/{settings.keycloak_realm}"
JWKS_URL = f"{ISSUER}/protocol/openid-connect/certs" 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 { NavLink } from "react-router";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import "./index.css"; import "./index.css";
import { Button, Group, Loader } from "@mantine/core"; import { Group, Loader } from "@mantine/core";
import { Config } from "@/config/config"; import { Config } from "@/config/config";
import { useCurrentUser, useLogoutUser } from "@/services/api"; import { useAuth } from "@/services/auth/AuthProvider";
export function Navbar() { export function Navbar() {
const { data: user, isLoading } = useCurrentUser(); const { loggedUser: user, isLoading } = useAuth();
const logout = useLogoutUser();
if (!user && isLoading) { if (!user && isLoading) {
return ( return (
<Group align="center" justify="center" h="80vh" w="100%"> <Group align="center" justify="center" h="80vh" w="100%">
@@ -37,15 +37,14 @@ export function Navbar() {
{t("login with keycloak", { capfirst: true })} {t("login with keycloak", { capfirst: true })}
</NavLink> </NavLink>
) : ( ) : (
<Button <a
href={`${Config.backend_uri}/auth/logout`}
className={"navLink"} className={"navLink"}
aria-label={t("logout", { capfirst: true })} aria-label={t("logout", { capfirst: true })}
onClick={() => {
logout.mutate();
}}
> >
{t("logout", { capfirst: true })} {t("logout", { capfirst: true })}
</Button> </a>
)} )}
</nav> </nav>
); );

View File

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

View File

@@ -8,15 +8,18 @@ import { Notifications } from "@mantine/notifications";
import "@mantine/core/styles.css"; import "@mantine/core/styles.css";
import "@mantine/dates/styles.css"; import "@mantine/dates/styles.css";
import "@mantine/notifications/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( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<MantineProvider> <MantineProvider>
<Notifications /> <Notifications />
<AuthProvider>
<RouterProvider router={router} /> <RouterProvider router={router} />
</AuthProvider>
</MantineProvider> </MantineProvider>
</QueryClientProvider> </QueryClientProvider>
</StrictMode>, </StrictMode>,

View File

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

View File

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

View File

@@ -25,12 +25,40 @@ import type { Contract, ContractCreate } from "./resources/contracts";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import { t } from "@/config/i18n"; 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> { export function useGetShipments(filters?: URLSearchParams): UseQueryResult<Shipment[], Error> {
const queryString = filters?.toString(); const queryString = filters?.toString();
return useQuery<Shipment[]>({ return useQuery<Shipment[]>({
queryKey: ["shipments", queryString], queryKey: ["shipments", queryString],
queryFn: () => queryFn: () =>
fetch(`${Config.backend_uri}/shipments${filters ? `?${queryString}` : ""}`, { fetchWithAuth(`${Config.backend_uri}/shipments${filters ? `?${queryString}` : ""}`, {
credentials: "include", credentials: "include",
}).then((res) => res.json()), }).then((res) => res.json()),
}); });
@@ -43,7 +71,7 @@ export function useGetShipment(
return useQuery<Shipment>({ return useQuery<Shipment>({
queryKey: ["shipment"], queryKey: ["shipment"],
queryFn: () => queryFn: () =>
fetch(`${Config.backend_uri}/shipments/${id}`, { fetchWithAuth(`${Config.backend_uri}/shipments/${id}`, {
credentials: "include", credentials: "include",
}).then((res) => res.json()), }).then((res) => res.json()),
enabled: !!id, enabled: !!id,
@@ -56,7 +84,7 @@ export function useCreateShipment() {
return useMutation({ return useMutation({
mutationFn: (newShipment: ShipmentCreate) => { mutationFn: (newShipment: ShipmentCreate) => {
return fetch(`${Config.backend_uri}/shipments`, { return fetchWithAuth(`${Config.backend_uri}/shipments`, {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -89,7 +117,7 @@ export function useEditShipment() {
return useMutation({ return useMutation({
mutationFn: ({ shipment, id }: ShipmentEditPayload) => { mutationFn: ({ shipment, id }: ShipmentEditPayload) => {
return fetch(`${Config.backend_uri}/shipments/${id}`, { return fetchWithAuth(`${Config.backend_uri}/shipments/${id}`, {
method: "PUT", method: "PUT",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -121,7 +149,7 @@ export function useDeleteShipment() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (id: number) => { mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/shipments/${id}`, { return fetchWithAuth(`${Config.backend_uri}/shipments/${id}`, {
method: "DELETE", method: "DELETE",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -153,9 +181,11 @@ export function useGetProductors(filters?: URLSearchParams): UseQueryResult<Prod
return useQuery<Productor[]>({ return useQuery<Productor[]>({
queryKey: ["productors", queryString], queryKey: ["productors", queryString],
queryFn: () => queryFn: () =>
fetch(`${Config.backend_uri}/productors${filters ? `?${queryString}` : ""}`, { fetchWithAuth(`${Config.backend_uri}/productors${filters ? `?${queryString}` : ""}`, {
credentials: "include", credentials: "include",
}).then((res) => res.json()), })
.then((res) => res.json()
),
}); });
} }
@@ -166,7 +196,7 @@ export function useGetProductor(
return useQuery<Productor>({ return useQuery<Productor>({
queryKey: ["productor"], queryKey: ["productor"],
queryFn: () => queryFn: () =>
fetch(`${Config.backend_uri}/productors/${id}`, { fetchWithAuth(`${Config.backend_uri}/productors/${id}`, {
credentials: "include", credentials: "include",
}).then((res) => res.json()), }).then((res) => res.json()),
enabled: !!id, enabled: !!id,
@@ -179,7 +209,7 @@ export function useCreateProductor() {
return useMutation({ return useMutation({
mutationFn: (newProductor: ProductorCreate) => { mutationFn: (newProductor: ProductorCreate) => {
return fetch(`${Config.backend_uri}/productors`, { return fetchWithAuth(`${Config.backend_uri}/productors`, {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -212,7 +242,7 @@ export function useEditProductor() {
return useMutation({ return useMutation({
mutationFn: ({ productor, id }: ProductorEditPayload) => { mutationFn: ({ productor, id }: ProductorEditPayload) => {
return fetch(`${Config.backend_uri}/productors/${id}`, { return fetchWithAuth(`${Config.backend_uri}/productors/${id}`, {
method: "PUT", method: "PUT",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -244,7 +274,7 @@ export function useDeleteProductor() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (id: number) => { mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/productors/${id}`, { return fetchWithAuth(`${Config.backend_uri}/productors/${id}`, {
method: "DELETE", method: "DELETE",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -278,7 +308,7 @@ export function useGetForm(
return useQuery<Form>({ return useQuery<Form>({
queryKey: ["form"], queryKey: ["form"],
queryFn: () => queryFn: () =>
fetch(`${Config.backend_uri}/forms/${id}`, { fetchWithAuth(`${Config.backend_uri}/forms/${id}`, {
credentials: "include", credentials: "include",
}).then((res) => res.json()), }).then((res) => res.json()),
enabled: !!id, enabled: !!id,
@@ -291,9 +321,7 @@ export function useGetForms(filters?: URLSearchParams): UseQueryResult<Form[], E
return useQuery<Form[]>({ return useQuery<Form[]>({
queryKey: ["forms", queryString], queryKey: ["forms", queryString],
queryFn: () => queryFn: () =>
fetch(`${Config.backend_uri}/forms${filters ? `?${queryString}` : ""}`, { fetch(`${Config.backend_uri}/forms${filters ? `?${queryString}` : ""}`).then((res) => res.json()),
credentials: "include",
}).then((res) => res.json()),
}); });
} }
@@ -302,7 +330,7 @@ export function useCreateForm() {
return useMutation({ return useMutation({
mutationFn: (newForm: FormCreate) => { mutationFn: (newForm: FormCreate) => {
return fetch(`${Config.backend_uri}/forms`, { return fetchWithAuth(`${Config.backend_uri}/forms`, {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -321,7 +349,7 @@ export function useDeleteForm() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (id: number) => { mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/forms/${id}`, { return fetchWithAuth(`${Config.backend_uri}/forms/${id}`, {
method: "DELETE", method: "DELETE",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -353,7 +381,7 @@ export function useEditForm() {
return useMutation({ return useMutation({
mutationFn: ({ id, form }: FormEditPayload) => { mutationFn: ({ id, form }: FormEditPayload) => {
return fetch(`${Config.backend_uri}/forms/${id}`, { return fetchWithAuth(`${Config.backend_uri}/forms/${id}`, {
method: "PUT", method: "PUT",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -387,7 +415,7 @@ export function useGetProduct(
return useQuery<Product>({ return useQuery<Product>({
queryKey: ["product"], queryKey: ["product"],
queryFn: () => queryFn: () =>
fetch(`${Config.backend_uri}/products/${id}`, { fetchWithAuth(`${Config.backend_uri}/products/${id}`, {
credentials: "include", credentials: "include",
}).then((res) => res.json()), }).then((res) => res.json()),
enabled: !!id, enabled: !!id,
@@ -400,7 +428,7 @@ export function useGetProducts(filters?: URLSearchParams): UseQueryResult<Produc
return useQuery<Product[]>({ return useQuery<Product[]>({
queryKey: ["products", queryString], queryKey: ["products", queryString],
queryFn: () => queryFn: () =>
fetch(`${Config.backend_uri}/products${filters ? `?${queryString}` : ""}`, { fetchWithAuth(`${Config.backend_uri}/products${filters ? `?${queryString}` : ""}`, {
credentials: "include", credentials: "include",
}).then((res) => res.json()), }).then((res) => res.json()),
}); });
@@ -411,7 +439,7 @@ export function useCreateProduct() {
return useMutation({ return useMutation({
mutationFn: (newProduct: ProductCreate) => { mutationFn: (newProduct: ProductCreate) => {
return fetch(`${Config.backend_uri}/products`, { return fetchWithAuth(`${Config.backend_uri}/products`, {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -443,7 +471,7 @@ export function useDeleteProduct() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (id: number) => { mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/products/${id}`, { return fetchWithAuth(`${Config.backend_uri}/products/${id}`, {
method: "DELETE", method: "DELETE",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -475,7 +503,7 @@ export function useEditProduct() {
return useMutation({ return useMutation({
mutationFn: ({ id, product }: ProductEditPayload) => { mutationFn: ({ id, product }: ProductEditPayload) => {
return fetch(`${Config.backend_uri}/products/${id}`, { return fetchWithAuth(`${Config.backend_uri}/products/${id}`, {
method: "PUT", method: "PUT",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -510,7 +538,7 @@ export function useGetUser(
return useQuery<User>({ return useQuery<User>({
queryKey: ["user"], queryKey: ["user"],
queryFn: () => queryFn: () =>
fetch(`${Config.backend_uri}/users/${id}`, { fetchWithAuth(`${Config.backend_uri}/users/${id}`, {
credentials: "include", credentials: "include",
}).then((res) => res.json()), }).then((res) => res.json()),
enabled: !!id, enabled: !!id,
@@ -523,7 +551,7 @@ export function useGetUsers(filters?: URLSearchParams): UseQueryResult<User[], E
return useQuery<User[]>({ return useQuery<User[]>({
queryKey: ["users", queryString], queryKey: ["users", queryString],
queryFn: () => queryFn: () =>
fetch(`${Config.backend_uri}/users${filters ? `?${queryString}` : ""}`, { fetchWithAuth(`${Config.backend_uri}/users${filters ? `?${queryString}` : ""}`, {
credentials: "include", credentials: "include",
}).then((res) => res.json()), }).then((res) => res.json()),
}); });
@@ -534,7 +562,7 @@ export function useCreateUser() {
return useMutation({ return useMutation({
mutationFn: (newUser: UserCreate) => { mutationFn: (newUser: UserCreate) => {
return fetch(`${Config.backend_uri}/users`, { return fetchWithAuth(`${Config.backend_uri}/users`, {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -566,7 +594,7 @@ export function useDeleteUser() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (id: number) => { mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/users/${id}`, { return fetchWithAuth(`${Config.backend_uri}/users/${id}`, {
method: "DELETE", method: "DELETE",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -598,7 +626,7 @@ export function useEditUser() {
return useMutation({ return useMutation({
mutationFn: ({ id, user }: UserEditPayload) => { mutationFn: ({ id, user }: UserEditPayload) => {
return fetch(`${Config.backend_uri}/users/${id}`, { return fetchWithAuth(`${Config.backend_uri}/users/${id}`, {
method: "PUT", method: "PUT",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -630,7 +658,7 @@ export function useGetContracts(filters?: URLSearchParams): UseQueryResult<Contr
return useQuery<Contract[]>({ return useQuery<Contract[]>({
queryKey: ["contracts", queryString], queryKey: ["contracts", queryString],
queryFn: () => queryFn: () =>
fetch(`${Config.backend_uri}/contracts${filters ? `?${queryString}` : ""}`, { fetchWithAuth(`${Config.backend_uri}/contracts${filters ? `?${queryString}` : ""}`, {
credentials: "include", credentials: "include",
}).then((res) => res.json()), }).then((res) => res.json()),
}); });
@@ -643,7 +671,7 @@ export function useGetContract(
return useQuery<Contract>({ return useQuery<Contract>({
queryKey: ["contract"], queryKey: ["contract"],
queryFn: () => queryFn: () =>
fetch(`${Config.backend_uri}/contracts/${id}`, { fetchWithAuth(`${Config.backend_uri}/contracts/${id}`, {
credentials: "include", credentials: "include",
}).then((res) => res.json()), }).then((res) => res.json()),
enabled: !!id, enabled: !!id,
@@ -654,7 +682,7 @@ export function useGetContract(
export function useGetContractFile() { export function useGetContractFile() {
return useMutation({ return useMutation({
mutationFn: async (id: number) => { 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", credentials: "include",
}).then((res) => res); }).then((res) => res);
@@ -682,7 +710,7 @@ export function useGetContractFile() {
export function useGetAllContractFile() { export function useGetAllContractFile() {
return useMutation({ return useMutation({
mutationFn: async (form_id: number) => { 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", credentials: "include",
}).then((res) => res); }).then((res) => res);
@@ -714,7 +742,6 @@ export function useCreateContract() {
mutationFn: (newContract: ContractCreate) => { mutationFn: (newContract: ContractCreate) => {
return fetch(`${Config.backend_uri}/contracts`, { return fetch(`${Config.backend_uri}/contracts`, {
method: "POST", method: "POST",
credentials: "include",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
@@ -737,7 +764,7 @@ export function useDeleteContract() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (id: number) => { mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/contracts/${id}`, { return fetchWithAuth(`${Config.backend_uri}/contracts/${id}`, {
method: "DELETE", method: "DELETE",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -769,7 +796,7 @@ export function useGetRoles(filters?: URLSearchParams): UseQueryResult<Role[], E
return useQuery<Role[]>({ return useQuery<Role[]>({
queryKey: ["roles", queryString], queryKey: ["roles", queryString],
queryFn: () => queryFn: () =>
fetch(`${Config.backend_uri}/users/roles${filters ? `?${queryString}` : ""}`, { fetchWithAuth(`${Config.backend_uri}/users/roles${filters ? `?${queryString}` : ""}`, {
credentials: "include", credentials: "include",
}).then((res) => res.json()), }).then((res) => res.json()),
}); });
@@ -784,25 +811,5 @@ export function useCurrentUser() {
}).then((res) => res.json()); }).then((res) => res.json());
}, },
retry: false, 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 { Group, Loader } from "@mantine/core";
import { Navigate, Outlet } from "react-router"; import { Navigate, Outlet } from "react-router";
import { useAuth } from "../AuthProvider";
export function Auth() { export function ProtectedRoute() {
const { data: userLogged, isLoading } = useCurrentUser(); const { loggedUser, isLoading } = useAuth();
if (!userLogged && isLoading) if (!loggedUser && isLoading)
return ( return (
<Group align="center" justify="center" h="80vh" w="100%"> <Group align="center" justify="center" h="80vh" w="100%">
<Loader color="pink" /> <Loader color="pink" />
</Group> </Group>
); );
if (!userLogged?.logged) { if (!loggedUser?.logged) {
return <Navigate to="/" replace />; return <Navigate to="/" replace />;
} }