From adb8a0e7edeb6a65e446d5a084f0c521c575dd30 Mon Sep 17 00:00:00 2001 From: "tristan-mihai.radulescu" Date: Sat, 19 Oct 2024 17:50:26 +0200 Subject: [PATCH] feat(GIST-56): add refresh token mechanism --- src/app/(gistLayout)/layout.tsx | 66 ++++----- src/components/api/api-provider.tsx | 6 +- .../feature/gist-details-wrapper.tsx | 133 +++++++++--------- src/lib/queries/auth.queries.tsx | 9 ++ src/lib/queries/orgs.queries.tsx | 4 + src/lib/queries/queries.tsx | 24 +++- 6 files changed, 138 insertions(+), 104 deletions(-) diff --git a/src/app/(gistLayout)/layout.tsx b/src/app/(gistLayout)/layout.tsx index 5622287..abe50e6 100644 --- a/src/app/(gistLayout)/layout.tsx +++ b/src/app/(gistLayout)/layout.tsx @@ -1,63 +1,65 @@ -'use client' - -import { ReactNode, useCallback } from 'react' -import GistLayout from './layout-ui' -import { useMe } from '@/lib/queries/user.queries' -import { useToast } from '@/components/shadcn/use-toast' -import { useCreateGist } from '@/lib/queries/gists.queries' -import { useCreateOrg } from '@/lib/queries/orgs.queries' - -export default function GistLayoutFeature({ children }: { children: ReactNode }) { - const { data } = useMe() - const { toast } = useToast() +"use client"; + +import { ReactNode, useCallback } from "react"; +import GistLayout from "./layout-ui"; +import { useMe } from "@/lib/queries/user.queries"; +import { useToast } from "@/components/shadcn/use-toast"; +import { useCreateGist } from "@/lib/queries/gists.queries"; +import { useCreateOrg } from "@/lib/queries/orgs.queries"; + +export default function GistLayoutFeature({ + children, +}: { + children: ReactNode; +}) { + const { data, error } = useMe(); + const { toast } = useToast(); const { mutate: createGist } = useCreateGist({ onSuccess: () => { toast({ - title: 'Gist Created', - description: 'Your gist has been created successfully', - }) + title: "Gist Created", + description: "Your gist has been created successfully", + }); }, - }) + }); const { mutate: createTeam } = useCreateOrg({ onSuccess: () => { toast({ - title: 'Team Created', - description: 'Your team has been created successfully', - }) + title: "Team Created", + description: "Your team has been created successfully", + }); }, - }) + }); - const onMyGistsClick = () => { - } + const onMyGistsClick = () => {}; const onCreateTeamClick = useCallback( (name: string) => { - createTeam(name) + createTeam(name); }, - [toast, createTeam] - ) + [toast, createTeam], + ); - const onLogoutClick = () => { - } + const onLogoutClick = () => {}; const onCreateGistClick = (name: string, content: string) => { createGist({ content, name, - }) - } + }); + }; return ( {children} - ) + ); } diff --git a/src/components/api/api-provider.tsx b/src/components/api/api-provider.tsx index 6134fe1..57705e2 100644 --- a/src/components/api/api-provider.tsx +++ b/src/components/api/api-provider.tsx @@ -1,5 +1,6 @@ "use client"; +import getQueryClient from "@/lib/queries/queries"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useState } from "react"; @@ -8,8 +9,9 @@ export default function QueryProvider({ }: { children: React.ReactNode; }) { - const [queryClient] = useState(() => new QueryClient()); return ( - {children} + + {children} + ); } diff --git a/src/components/feature/gist-details-wrapper.tsx b/src/components/feature/gist-details-wrapper.tsx index 4f1ec77..829db2e 100644 --- a/src/components/feature/gist-details-wrapper.tsx +++ b/src/components/feature/gist-details-wrapper.tsx @@ -1,11 +1,11 @@ -'use client' +"use client"; -import { useState, useEffect, useCallback } from 'react' -import { Gist } from '@/types' -import GistLanding from '@/components/ui/gist-landing' -import { toast } from '../shadcn/use-toast' -import { useRouter } from 'next/navigation' -import { useKeyPress } from '@/lib/hook/use-key-press' +import { useState, useEffect, useCallback } from "react"; +import { Gist } from "@/types"; +import GistLanding from "@/components/ui/gist-landing"; +import { toast } from "../shadcn/use-toast"; +import { useRouter } from "next/navigation"; +import { useKeyPress } from "@/lib/hook/use-key-press"; console.log(` _______ ________ ______ _________ ______ ________ ______ ______ @@ -17,101 +17,100 @@ console.log(` \\_____\\/ \\________\\/ \\_____\\/ \\__\\/ \\_____\\/ \\:_\\/ \\__\\/\\__\\/ \\_\\/ \\_\\/ \n Your snippet vault - `) + `); export default function GistDetailsWrapper() { - const router = useRouter() + const router = useRouter(); const [gist, setGist] = useState({ - id: 'example', - name: 'Gist example', - code: '', - }) - const [isShareDialogOpen, setIsShareDialogOpen] = useState(false) + id: "example", + name: "Gist example", + code: "", + }); + const [isShareDialogOpen, setIsShareDialogOpen] = useState(false); useEffect(() => { - const storedGistName = localStorage.getItem('gistName') || 'Gist example' - const storedGistCode = localStorage.getItem('gistCode') || '' + const storedGistName = localStorage.getItem("gistName") || "Gist example"; + const storedGistCode = localStorage.getItem("gistCode") || ""; setGist((prevGist) => ({ ...prevGist, name: storedGistName, code: storedGistCode, - })) - }, []) + })); + }, []); const handleDownload = useCallback((name: string, code: string) => { - const element = document.createElement('a') - const file = new Blob([code], { type: 'text/plain' }) - element.href = URL.createObjectURL(file) - element.download = name - document.body.appendChild(element) - element.click() - document.body.removeChild(element) + const element = document.createElement("a"); + const file = new Blob([code], { type: "text/plain" }); + element.href = URL.createObjectURL(file); + element.download = name; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); toast({ - title: 'Gist Downloaded', - description: 'Your gist has been downloaded successfully', - }) - }, []) + title: "Gist Downloaded", + description: "Your gist has been downloaded successfully", + }); + }, []); - const handleShare = useCallback(() => { - }, []) + const handleShare = useCallback(() => {}, []); const handleLogin = useCallback(() => { - router.push('/login') - }, [router]) + router.push("/login"); + }, [router]); const handleShareDialog = useCallback(() => { - setIsShareDialogOpen(true) - }, []) + setIsShareDialogOpen(true); + }, []); const handleGistNameChange = useCallback((newName: string) => { - setGist((prevGist) => ({ ...prevGist, name: newName })) - localStorage.setItem('gistName', newName) - }, []) + setGist((prevGist) => ({ ...prevGist, name: newName })); + localStorage.setItem("gistName", newName); + }, []); const handleGistCodeChange = useCallback((newCode: string) => { - setGist((prevGist) => ({ ...prevGist, code: newCode })) - localStorage.setItem('gistCode', newCode) - }, []) + setGist((prevGist) => ({ ...prevGist, code: newCode })); + localStorage.setItem("gistCode", newCode); + }, []); const handleOpenFile = useCallback(() => { - const fileInput = document.createElement('input') - fileInput.type = 'file' + const fileInput = document.createElement("input"); + fileInput.type = "file"; fileInput.onchange = (event: Event) => { - const target = event.target as HTMLInputElement - const file = target.files?.[0] + const target = event.target as HTMLInputElement; + const file = target.files?.[0]; if (file) { - handleGistNameChange(file.name) - const reader = new FileReader() + handleGistNameChange(file.name); + const reader = new FileReader(); reader.onload = (e) => { - const fileContent = e.target?.result as string - handleGistCodeChange(fileContent) - } - reader.readAsText(file) + const fileContent = e.target?.result as string; + handleGistCodeChange(fileContent); + }; + reader.readAsText(file); } - } - fileInput.click() - }, [handleGistNameChange, handleGistCodeChange]) + }; + fileInput.click(); + }, [handleGistNameChange, handleGistCodeChange]); const handleKeyPressDownload = useCallback( (e: KeyboardEvent) => { - e.preventDefault() - handleDownload(gist.name, gist.code) + e.preventDefault(); + handleDownload(gist.name, gist.code); }, - [gist.name, gist.code, handleDownload] - ) + [gist.name, gist.code, handleDownload], + ); const handleKeyPressOpenFile = useCallback( (e: KeyboardEvent) => { - e.preventDefault() - handleOpenFile() + e.preventDefault(); + handleOpenFile(); }, - [handleOpenFile] - ) + [handleOpenFile], + ); - useKeyPress('d', handleKeyPressDownload, ['ctrlKey']) - useKeyPress('l', handleLogin, ['ctrlKey']) - useKeyPress('o', handleKeyPressOpenFile, ['ctrlKey']) - useKeyPress('s', handleShareDialog, ['ctrlKey', 'shiftKey']) + useKeyPress("d", handleKeyPressDownload, ["ctrlKey"]); + useKeyPress("l", handleLogin, ["ctrlKey"]); + useKeyPress("o", handleKeyPressOpenFile, ["ctrlKey"]); + useKeyPress("s", handleShareDialog, ["ctrlKey", "shiftKey"]); return ( - ) + ); } diff --git a/src/lib/queries/auth.queries.tsx b/src/lib/queries/auth.queries.tsx index 7b121fc..37fd2d3 100644 --- a/src/lib/queries/auth.queries.tsx +++ b/src/lib/queries/auth.queries.tsx @@ -30,6 +30,15 @@ const fetchLocalAuthVerify = async ({ return json; }; +export const renewToken = async () => { + const json = await ky + .post(`${getBackendURL()}/auth/identity/renew`, { + credentials: "include", + }) + .json(); + return json; +}; + /** * Start the local authentication process by sending an email to the user * @param email, the user email diff --git a/src/lib/queries/orgs.queries.tsx b/src/lib/queries/orgs.queries.tsx index b051a8b..e401d43 100644 --- a/src/lib/queries/orgs.queries.tsx +++ b/src/lib/queries/orgs.queries.tsx @@ -24,6 +24,10 @@ const fetchOrgs = async () => { }) .json(); + if (!json) { + return []; + } + for (let org of json) { let team: Team = { id: org.id, diff --git a/src/lib/queries/queries.tsx b/src/lib/queries/queries.tsx index 0a5442f..6bdd2e3 100644 --- a/src/lib/queries/queries.tsx +++ b/src/lib/queries/queries.tsx @@ -1,8 +1,26 @@ "use client"; -import { QueryClient } from "@tanstack/react-query"; -import { cache } from "react"; +import { toast } from "@/components/shadcn/use-toast"; +import { QueryCache, QueryClient } from "@tanstack/react-query"; +import { renewToken } from "./auth.queries"; export const getQueryClient = () => { - return new QueryClient(); + return new QueryClient({ + queryCache: new QueryCache({ + onError: async (error, query) => { + if (error.message.includes("401")) { + try { + await renewToken(); + query.fetch(); //try to refetch + } catch { + // need to logout !!!! + toast({ + title: "Error", + description: "Please try to log in again", + }); + } + } + }, + }), + }); }; export default getQueryClient;