From 634f422528fcb47df361ff54467f3783433329b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Rafael=20Siqueira?= Date: Fri, 21 Feb 2025 14:56:02 -0300 Subject: [PATCH] add auth --- dashboard_js/app/components/form.jsx | 145 ++++++++++++++++++ dashboard_js/app/components/loader.jsx | 27 ++++ dashboard_js/app/hooks/use-auth.tsx | 27 ++-- dashboard_js/app/layouts/app.tsx | 43 +++++- dashboard_js/app/layouts/auth.tsx | 14 ++ dashboard_js/app/root.tsx | 12 +- dashboard_js/app/routes/auth/signin/index.tsx | 35 ++++- dashboard_js/package-lock.json | 65 +++++++- dashboard_js/package.json | 6 + 9 files changed, 335 insertions(+), 39 deletions(-) create mode 100644 dashboard_js/app/components/form.jsx create mode 100644 dashboard_js/app/components/loader.jsx diff --git a/dashboard_js/app/components/form.jsx b/dashboard_js/app/components/form.jsx new file mode 100644 index 0000000..16ea419 --- /dev/null +++ b/dashboard_js/app/components/form.jsx @@ -0,0 +1,145 @@ +import React, { forwardRef, useContext, useId } from 'react' +import { ExclamationCircleIcon } from '@heroicons/react/24/outline' +import { omit } from 'ramda' +import Loader from './loader' +import clsx from 'clsx' + +const ControlContext = React.createContext({}) + +function ControlProvider({ children, ...props }) { + return ( + {children} + ) +} + +function useControl(props) { + const field = useContext(ControlContext) + return { ...field, ...props } +} + +export const Control = forwardRef(function Control( + { as = 'div', children, ...props }, + ref, +) { + const id = useId() + const props_ = omit(['id'], props) + + return ( + + {React.createElement(as, { ref, ...props }, children)} + + ) +}) + +export function Button({ + children, + as = 'button', + className, + isLoading = false, + ...props +}) { + if (isLoading) { + props['disabled'] = isLoading + } + + return React.createElement( + as, + { + className: clsx( + 'font-medium text-green-primary rounded-lg bg-green-secondary hover:bg-green-support', + 'h-12 px-4 relative', + 'disabled:bg-green-secondary/50 disabled:pointer-events-none', + 'transition', + className, + ), + ...props, + }, + <> + {isLoading && ( +
+ +
+ )} + + {children} + , + ) +} + +export function Label({ children, className, ...props }) { + const { id, htmlFor, className: _, ...field } = useControl(props) + + return ( + + ) +} + +export const Input = forwardRef(function Input( + { as = 'input', size = 'base', className, children, ...props }, + ref, +) { + const { className: _, ...field } = useControl(props) + const sizes = { base: 'h-12' } + + return React.createElement( + as, + { + className: clsx( + 'bg-white outline-none px-4 rounded-lg transition', + 'border border-green-light dark:border-gray-700 dark:bg-gray-800', + 'focus:ring-1 focus:border-green-secondary focus:ring-green-secondary focus:placeholder:text-transparent', + // Tailwind's won't inherit focus behavior; you must define it explicitly for both modes. + 'dark:focus:border-green-secondary', + 'aria-[invalid=true]:border-red-400 aria-[invalid=true]:ring-red-400', + 'dark:aria-[invalid=true]:border-red-500 dark:aria-[invalid=true]:ring-red-500', + 'disabled:text-gray-400 disabled:border-gray-300 disabled:bg-gray-200', + 'dark:disabled:text-gray-500 dark:disabled:bg-gray-700', + sizes?.[size], + className, + ), + ref, + ...field, + }, + children, + ) +}) + +export const Checkbox = forwardRef(function Checkbox( + { className, ...props }, + ref, +) { + const { className: _, ...field } = useControl(props) + + return ( + + ) +}) + +export function Error({ children }) { + return ( +
+ +
{children}
+
+ ) +} diff --git a/dashboard_js/app/components/loader.jsx b/dashboard_js/app/components/loader.jsx new file mode 100644 index 0000000..2942d94 --- /dev/null +++ b/dashboard_js/app/components/loader.jsx @@ -0,0 +1,27 @@ +import clsx from 'clsx' + +export default function Loader({ className, ...props }) { + return ( + + + + + ) +} diff --git a/dashboard_js/app/hooks/use-auth.tsx b/dashboard_js/app/hooks/use-auth.tsx index dbe391a..ed40550 100644 --- a/dashboard_js/app/hooks/use-auth.tsx +++ b/dashboard_js/app/hooks/use-auth.tsx @@ -16,7 +16,7 @@ interface AuthContextType { username: string password: string }) => Promise - currentUser: () => Promise + signOut: () => Promise } const AuthContext = createContext(null) @@ -29,17 +29,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const [authUser, setAuthUser] = useState(null) - const currentUser = useCallback(async () => { - try { - const currentUser = await Auth.fetchUserAttributes() - - setAuthUser(currentUser) - return currentUser - } catch { - setAuthUser(null) - } - }, []) - const signIn = useCallback( async ({ username, @@ -65,16 +54,22 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { [], ) - const ctxValue = useMemo( + const signOut = useCallback(async (): Promise => { + try { + return await Auth.signOut() + } catch {} + }, []) + + const authContext = useMemo( () => ({ authUser, signIn, - currentUser, + signOut, }), - [authUser, signIn, currentUser], + [authUser, signIn, signOut], ) return ( - {children} + {children} ) } diff --git a/dashboard_js/app/layouts/app.tsx b/dashboard_js/app/layouts/app.tsx index 5ebd05e..cde43f3 100644 --- a/dashboard_js/app/layouts/app.tsx +++ b/dashboard_js/app/layouts/app.tsx @@ -1,20 +1,47 @@ -import { useNavigation } from 'react-router' +import { useNavigation, redirect, useNavigate } from 'react-router' +import { fetchAuthSession } from 'aws-amplify/auth' import { Link } from 'react-router' import { Outlet } from 'react-router' +import { useAuth } from '~/hooks/use-auth' + +export async function clientLoader() { + const session = await fetchAuthSession() + + if (!session?.tokens?.idToken) { + throw redirect('/auth') + } +} export default function Layout() { const navigation = useNavigation() + const navigate = useNavigate() + const { signOut } = useAuth() const isNavigating = Boolean(navigation.location) return ( <> -
    -
  • - Pagamentos - Usuários - Matrículas -
  • -
+ {isNavigating ? <>Loading... : } diff --git a/dashboard_js/app/layouts/auth.tsx b/dashboard_js/app/layouts/auth.tsx index 337ed2f..ba91e1c 100644 --- a/dashboard_js/app/layouts/auth.tsx +++ b/dashboard_js/app/layouts/auth.tsx @@ -49,3 +49,17 @@ export default function Auth() { ) } + +export function Card({ children }: { children: React.ReactNode }) { + return ( + +
+ {children} +
+
+ ) +} + +export function Container({ children }: { children: React.ReactNode }) { + return
{children}
+} diff --git a/dashboard_js/app/root.tsx b/dashboard_js/app/root.tsx index cd894f2..b2b236b 100644 --- a/dashboard_js/app/root.tsx +++ b/dashboard_js/app/root.tsx @@ -10,6 +10,7 @@ import { import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { Amplify } from 'aws-amplify' import { AuthProvider } from '~/hooks/use-auth' +import { Smallest as Logo } from '~/components/logo' import amplifyconfig from './amplifyconfiguration.json' Amplify.configure(amplifyconfig) @@ -59,16 +60,7 @@ export default function App() { export function HydrateFallback() { return ( -
-
- - -
+ ) } diff --git a/dashboard_js/app/routes/auth/signin/index.tsx b/dashboard_js/app/routes/auth/signin/index.tsx index 73af6ef..d047f6e 100644 --- a/dashboard_js/app/routes/auth/signin/index.tsx +++ b/dashboard_js/app/routes/auth/signin/index.tsx @@ -1,8 +1,35 @@ +import { useForm } from 'react-hook-form' +import { useAuth } from '~/hooks/use-auth' +import { Card } from '~/layouts/auth' +import { Control, Label, Input, Button } from '~/components/form' +import { useNavigate } from 'react-router' + export default function Signin() { + const navigate = useNavigate() + const { register, handleSubmit, formState } = useForm() + const { signIn } = useAuth() + + const onSubmit = async (payload) => { + const { nextStep } = await signIn(payload) + return navigate('/') + } + return ( -
- Username - -
+ +
+ + + + + + + + + + +
+
) } diff --git a/dashboard_js/package-lock.json b/dashboard_js/package-lock.json index 3914323..57836e2 100644 --- a/dashboard_js/package-lock.json +++ b/dashboard_js/package-lock.json @@ -6,15 +6,21 @@ "": { "name": "dashboard_js", "dependencies": { + "@heroicons/react": "^2.2.0", + "@hookform/error-message": "^2.0.1", + "@hookform/resolvers": "^4.1.0", "@react-router/fs-routes": "^7.2.0", "@react-router/node": "^7.2.0", "@react-router/serve": "^7.2.0", "@tanstack/react-query": "^5.66.9", "aws-amplify": "^6.13.1", "axios": "^1.7.9", + "clsx": "^2.1.1", "isbot": "^5.1.17", + "ramda": "^0.30.1", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hook-form": "^7.54.2", "react-router": "^7.2.0" }, "devDependencies": { @@ -2450,6 +2456,38 @@ "dev": true, "license": "MIT" }, + "node_modules/@heroicons/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16 || ^19.0.0-rc" + } + }, + "node_modules/@hookform/error-message": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hookform/error-message/-/error-message-2.0.1.tgz", + "integrity": "sha512-U410sAr92xgxT1idlu9WWOVjndxLdgPUHEB8Schr27C9eh7/xUnITWpCMF93s+lGiG++D4JnbSnrb5A21AdSNg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@hookform/resolvers": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.0.tgz", + "integrity": "sha512-fX/uHKb+OOCpACLc6enuTQsf0ZpRrKbeBBPETg5PCPLCIYV6osP2Bw6ezuclM61lH+wBF9eXcuC0+BFh9XOEnQ==", + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001698" + }, + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -5209,7 +5247,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7649,6 +7686,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ramda": { + "version": "0.30.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz", + "integrity": "sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ramda" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -7736,6 +7783,22 @@ "react": "^19.0.0" } }, + "node_modules/react-hook-form": { + "version": "7.54.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz", + "integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-hotkeys-hook": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.6.1.tgz", diff --git a/dashboard_js/package.json b/dashboard_js/package.json index 3f9d2b2..ce5736a 100644 --- a/dashboard_js/package.json +++ b/dashboard_js/package.json @@ -9,15 +9,21 @@ "typecheck": "react-router typegen && tsc" }, "dependencies": { + "@heroicons/react": "^2.2.0", + "@hookform/error-message": "^2.0.1", + "@hookform/resolvers": "^4.1.0", "@react-router/fs-routes": "^7.2.0", "@react-router/node": "^7.2.0", "@react-router/serve": "^7.2.0", "@tanstack/react-query": "^5.66.9", "aws-amplify": "^6.13.1", "axios": "^1.7.9", + "clsx": "^2.1.1", "isbot": "^5.1.17", + "ramda": "^0.30.1", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hook-form": "^7.54.2", "react-router": "^7.2.0" }, "devDependencies": {