From 22b09f07536e40fcfe8961efb4cd316f0f3345af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo=20Fricks?= Date: Sun, 31 Mar 2024 16:56:55 -0300 Subject: [PATCH 1/2] feat: Update authentication and session management - Introduce AuthContext for signIn, signOut, and user data management - Implement nookies library for Cookie handling - Update _app.tsx and signin page - Integrate AuthContext in the dashboard and handle token expiration in getServerSideProps - Enhance profile component to utilize AuthContext - Revise sidebar component - Expand auth services with user data handling function --- package-lock.json | 34 ++++++++++++++- package.json | 4 +- src/components/Header/Profile.tsx | 52 +++++++++++++---------- src/components/Sidebar/index.tsx | 21 +++++----- src/contexts/AuthContext.tsx | 65 ++++++++++++++++++++++++++++ src/pages/_app.tsx | 15 +++---- src/pages/auth/signin.tsx | 37 ++++++++-------- src/pages/dashboard.tsx | 70 +++++++++++++++++++++++++++++++ src/services/auth.ts | 19 ++++++++- 9 files changed, 252 insertions(+), 65 deletions(-) create mode 100644 src/contexts/AuthContext.tsx create mode 100644 src/pages/dashboard.tsx diff --git a/package-lock.json b/package-lock.json index bb981d5..9fed978 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "frontend", - "version": "0.1.7", + "version": "0.1.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "frontend", - "version": "0.1.7", + "version": "0.1.8", "dependencies": { "@chakra-ui/next-js": "^2.2.0", "@chakra-ui/react": "^2.8.2", @@ -17,6 +17,7 @@ "framer-motion": "^11.0.20", "next": "latest", "next-auth": "^4.24.7", + "nookies": "^2.5.2", "react": "latest", "react-dom": "latest", "react-hook-form": "^7.51.2", @@ -24,6 +25,7 @@ "yup": "^1.4.0" }, "devDependencies": { + "@types/cookie": "^0.6.0", "@types/node": "latest", "@types/react": "latest", "@types/react-dom": "latest", @@ -1855,6 +1857,12 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -4729,6 +4737,23 @@ } } }, + "node_modules/nookies": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/nookies/-/nookies-2.5.2.tgz", + "integrity": "sha512-x0TRSaosAEonNKyCrShoUaJ5rrT5KHRNZ5DwPCuizjgrnkpE5DRf3VL7AyyQin4htict92X1EQ7ejDbaHDVdYA==", + "dependencies": { + "cookie": "^0.4.1", + "set-cookie-parser": "^2.4.6" + } + }, + "node_modules/nookies/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/oauth": { "version": "0.9.15", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", @@ -5532,6 +5557,11 @@ "node": ">=10" } }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", diff --git a/package.json b/package.json index 2d27897..3a8a613 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "framer-motion": "^11.0.20", "next": "latest", "next-auth": "^4.24.7", + "nookies": "^2.5.2", "react": "latest", "react-dom": "latest", "react-hook-form": "^7.51.2", @@ -25,6 +26,7 @@ "yup": "^1.4.0" }, "devDependencies": { + "@types/cookie": "^0.6.0", "@types/node": "latest", "@types/react": "latest", "@types/react-dom": "latest", @@ -32,4 +34,4 @@ "eslint-config-next": "latest", "typescript": "latest" } -} \ No newline at end of file +} diff --git a/src/components/Header/Profile.tsx b/src/components/Header/Profile.tsx index 4771dd1..e677424 100644 --- a/src/components/Header/Profile.tsx +++ b/src/components/Header/Profile.tsx @@ -1,46 +1,52 @@ +import { useContext } from "react"; import { RiLogoutCircleLine } from "react-icons/ri"; -import { signOut, useSession } from "next-auth/react"; import { Box, Flex, Text, Menu, Avatar, MenuList, MenuItem, MenuButton } from "@chakra-ui/react"; +import { AuthContext } from "@/contexts/AuthContext"; + interface ProfileProps { showProfileData: boolean | undefined; } export function Profile({ showProfileData = true }: ProfileProps) { - const { data: session } = useSession(); - - console.log("session...", session); + const { user, signOut } = useContext(AuthContext); return ( {showProfileData && ( - {session?.user?.username ? session?.user?.username : "Sem nome"} + {user?.name} - {session?.user?.email ? session?.user?.email : "Sem e-mail"} + {user?.email} )} - + - {session && ( - - <> - } - onClick={() => signOut()} - color="gray.600" - bg="gray.900" - _hover={{ color: "white" }} - > - Logout - - {session.user.about} - - - )} + + + <> + } + onClick={signOut} + color="gray.600" + bg="gray.900" + _hover={{ color: "white" }} + > + Logout + + + {user?.about} + + + ); diff --git a/src/components/Sidebar/index.tsx b/src/components/Sidebar/index.tsx index 55e2ecd..7b7f3ee 100644 --- a/src/components/Sidebar/index.tsx +++ b/src/components/Sidebar/index.tsx @@ -1,5 +1,3 @@ -import { SidebarNav } from './SidebarNav' -import { useSidebarDrawer } from '../../contexts/SidebarDrawerContext' import { Box, Drawer, @@ -8,16 +6,19 @@ import { DrawerOverlay, DrawerContent, DrawerCloseButton, - useBreakpointValue, -} from '@chakra-ui/react' + useBreakpointValue +} from "@chakra-ui/react"; + +import { SidebarNav } from "./SidebarNav"; +import { useSidebarDrawer } from "@/contexts/SidebarDrawerContext"; export function Sidebar() { - const { isOpen, onClose } = useSidebarDrawer() + const { isOpen, onClose } = useSidebarDrawer(); const isDrawerSidebar = useBreakpointValue({ base: true, - lg: false, - }) + lg: false + }); if (isDrawerSidebar) { return ( @@ -32,12 +33,12 @@ export function Sidebar() { - ) + ); } return ( - + - ) + ); } diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx new file mode 100644 index 0000000..46edf11 --- /dev/null +++ b/src/contexts/AuthContext.tsx @@ -0,0 +1,65 @@ +import Router from "next/router"; +import { destroyCookie, parseCookies, setCookie } from "nookies"; +import { createContext, useEffect, useState } from "react"; + +import { signInRequest, userMe } from "../services/auth"; + +type SignInData = { + email: string; + password: string; +}; + +type User = { + name: string; + email: string; + about: string; +}; + +type AuthContextType = { + isAuthenticated: boolean; + user: User | null; + signIn: (data: SignInData) => Promise; + signOut: () => void; +}; + +export const AuthContext = createContext({} as AuthContextType); + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(null); + + const isAuthenticated = !!user; + + useEffect(() => { + const { "nextauth.token": token } = parseCookies(); + + if (token) { + userMe(token) + .then(response => { + setUser(response); + }) + .catch(() => { + signOut(); + }); + } + }, []); + + async function signIn({ email, password }: SignInData) { + const { user, jwt } = await signInRequest({ email, password }); + + setCookie(undefined, "nextauth.token", jwt, { + // maxAge: 60 * 60 * 1 // 1 hour + maxAge: 180 // 3 minutes + }); + + setUser(user); + + Router.push("/dashboard"); + } + + function signOut() { + destroyCookie(undefined, "nextauth.token"); + Router.push("/auth/signin"); + } + + return {children}; +} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 6cd8985..12e75a5 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,12 +1,12 @@ -import { theme } from "../styles/theme"; -import { fonts } from "@/lib/fonts"; -import { Session } from "next-auth"; import { AppProps } from "next/app"; import { ChakraProvider } from "@chakra-ui/react"; -import { SessionProvider } from "next-auth/react"; + +import { theme } from "@/styles/theme"; +import { fonts } from "@/lib/fonts"; +import { AuthProvider } from "@/contexts/AuthContext"; import { SidebarDrawerProvider } from "@/contexts/SidebarDrawerContext"; -function MyApp({ Component, pageProps }: AppProps<{ session: Session }>) { +function MyApp({ Component, pageProps }: AppProps) { return ( <> - + + - + ); } diff --git a/src/pages/auth/signin.tsx b/src/pages/auth/signin.tsx index 3259178..a600bbd 100644 --- a/src/pages/auth/signin.tsx +++ b/src/pages/auth/signin.tsx @@ -1,13 +1,15 @@ import * as yup from "yup"; -import { Input } from "../../components/Form/Input"; -import { Toast } from "@/components/Toast"; -import { signIn } from "next-auth/react"; -import { useRouter } from "next/router"; +import { useContext } from "react"; +import { AxiosError } from "axios"; import { yupResolver } from "@hookform/resolvers/yup"; -import { LogoSkateHub } from "@/components/LogoSkateHub"; import { SubmitHandler, useForm } from "react-hook-form"; import { Button, Flex, Stack, Text } from "@chakra-ui/react"; +import { Input } from "@/components/Form/Input"; +import { Toast } from "@/components/Toast"; +import { AuthContext } from "@/contexts/AuthContext"; +import { LogoSkateHub } from "@/components/LogoSkateHub"; + type SignInFormData = { email: string; password: string; @@ -19,7 +21,7 @@ const signInFormSchema = yup.object().shape({ }); export default function SignIn() { - const router = useRouter(); + const { signIn } = useContext(AuthContext); const { addToast } = Toast(); const { register, handleSubmit, formState } = useForm({ @@ -29,22 +31,17 @@ export default function SignIn() { const { errors } = formState; const handleSignIn: SubmitHandler = async values => { - const result = await signIn("credentials", { - redirect: false, - email: values.email, - password: values.password - }); + await signIn(values) + .then(_ => {}) + .catch((error: AxiosError) => { + console.error("error: ", error.message); - if (result?.status === 200) { - router.replace("/dashboard"); - return; - } else { - addToast({ - title: "Erro de autenticação.", - message: "Verifique seus dados de login e tente novamente.", - type: "error" + addToast({ + title: "Erro de autenticação.", + message: "Verifique seus dados de login e tente novamente.", + type: "error" + }); }); - } }; return ( diff --git a/src/pages/dashboard.tsx b/src/pages/dashboard.tsx new file mode 100644 index 0000000..1d5d569 --- /dev/null +++ b/src/pages/dashboard.tsx @@ -0,0 +1,70 @@ +import { useContext } from "react"; +import { parseCookies } from "nookies"; +import { Box, Flex, SimpleGrid, Text } from "@chakra-ui/react"; + +import { Header } from "@/components/Header"; +import { Sidebar } from "@/components/Sidebar"; +import { AuthContext } from "@/contexts/AuthContext"; + +export default function Dashboard() { + const { user } = useContext(AuthContext); + + return ( + <> + +
+ + + + + + Status do cadastro do atleta + + {user?.name} + Regular + + + + + Dados de acesso da semana + + {user?.name}
+ {user?.about}
+
+ + + + Documentações + + + Instruções para inscrição em eventos + + + + + + Taxa de abertura + + +
+
+ + + ); +} + +export const getServerSideProps = async (ctx: any) => { + const { ["nextauth.token"]: token } = parseCookies(ctx); + + if (!token) { + return { + redirect: { + destination: "/", + permanent: false + } + }; + } + return { + props: {} + }; +}; diff --git a/src/services/auth.ts b/src/services/auth.ts index 84e2eb5..b62144a 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -1,8 +1,13 @@ import axios from "axios"; -const strapiUrl = process.env.STRAPI_URL; +const strapiUrl = process.env.NEXT_PUBLIC_STRAPI_URL; -export async function signIn({ email, password }: { email: any; password: any }) { +type SignInData = { + email: string; + password: string; +}; + +export async function signInRequest({ email, password }: SignInData) { const res = await axios.post(`${strapiUrl}/api/auth/local`, { identifier: email, password @@ -10,3 +15,13 @@ export async function signIn({ email, password }: { email: any; password: any }) return res.data; } + +export async function userMe(token: string) { + const res = await axios.get(`${strapiUrl}/api/users/me`, { + headers: { + Authorization: `Bearer ${token}` + } + }); + + return res.data; +} From 6104e6a7e273e0e7ef530a7643421cc32e54c895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo=20Fricks?= Date: Sun, 31 Mar 2024 16:59:03 -0300 Subject: [PATCH 2/2] =?UTF-8?q?Update=20changelog=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + package.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ec8f0bf..93ad290 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ And that's it! Your `SkateHub Frontend` should now be up and running locally on ### 2024 +- 2024-03-31 - Update `authentication` and `session` management [#11](https://github.com/jpcmf/Frontend-GraduateProgram-FullStack-2024/pull/11) _(v0.1.9)_ - 2024-03-29 - Create the `toast` component [#10](https://github.com/jpcmf/Frontend-GraduateProgram-FullStack-2024/pull/10) _(v0.1.8)_ - 2024-03-27 - Add the `favicon` of the SkateHub project [#9](https://github.com/jpcmf/Frontend-GraduateProgram-FullStack-2024/pull/9) _(v0.1.7)_ - 2024-03-27 - Add the `prettier.config.js` file to the project to handle with code formatter [#8](https://github.com/jpcmf/Frontend-GraduateProgram-FullStack-2024/pull/8) _(v0.1.6)_ diff --git a/package.json b/package.json index 3a8a613..4494abf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "0.1.8", + "version": "0.1.9", "private": true, "scripts": { "dev": "next dev", @@ -34,4 +34,4 @@ "eslint-config-next": "latest", "typescript": "latest" } -} +} \ No newline at end of file