add auth
This commit is contained in:
145
dashboard_js/app/components/form.jsx
Normal file
145
dashboard_js/app/components/form.jsx
Normal file
@@ -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 (
|
||||
<ControlContext.Provider value={props}>{children}</ControlContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<ControlProvider id={props?.id || id} {...props_}>
|
||||
{React.createElement(as, { ref, ...props }, children)}
|
||||
</ControlProvider>
|
||||
)
|
||||
})
|
||||
|
||||
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 && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-green-secondary rounded-xl">
|
||||
<Loader className="w-5 text-white" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</>,
|
||||
)
|
||||
}
|
||||
|
||||
export function Label({ children, className, ...props }) {
|
||||
const { id, htmlFor, className: _, ...field } = useControl(props)
|
||||
|
||||
return (
|
||||
<label
|
||||
className={clsx(
|
||||
"black cursor-pointer aria-required:after:content-['*'] aria-required:after:ml-0.5 aria-required:after:text-red-500",
|
||||
className,
|
||||
)}
|
||||
htmlFor={htmlFor ?? id}
|
||||
{...field}
|
||||
>
|
||||
{children}
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<input
|
||||
type="checkbox"
|
||||
className={clsx(
|
||||
'text-green-secondary border border-gray-300',
|
||||
'focus:ring-2 focus:border-green-secondary focus:ring-green-secondary focus:ring-offset-0 focus:ring-opacity-30',
|
||||
'dark:border-gray-700 dark:bg-gray-800 focus:dark:border-security dark:checked:bg-green-secondary dark:checked:border-security dark:disabled:bg-gray-700',
|
||||
'disabled:bg-gray-200 outline-none rounded transition',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...field}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
export function Error({ children }) {
|
||||
return (
|
||||
<div className="text-sm text-red-500 flex items-start gap-0.5">
|
||||
<ExclamationCircleIcon className="w-4 mt-[1.5px] flex-shrink-0" />
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
27
dashboard_js/app/components/loader.jsx
Normal file
27
dashboard_js/app/components/loader.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import clsx from 'clsx'
|
||||
|
||||
export default function Loader({ className, ...props }) {
|
||||
return (
|
||||
<svg
|
||||
className={clsx('animate-spin', className)}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-50"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -16,7 +16,7 @@ interface AuthContextType {
|
||||
username: string
|
||||
password: string
|
||||
}) => Promise<Auth.SignInOutput>
|
||||
currentUser: () => Promise<Auth.FetchUserAttributesOutput | void>
|
||||
signOut: () => Promise<void>
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | null>(null)
|
||||
@@ -29,17 +29,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [authUser, setAuthUser] =
|
||||
useState<Auth.FetchUserAttributesOutput | null>(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<AuthContextType>(
|
||||
const signOut = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
return await Auth.signOut()
|
||||
} catch {}
|
||||
}, [])
|
||||
|
||||
const authContext = useMemo<AuthContextType>(
|
||||
() => ({
|
||||
authUser,
|
||||
signIn,
|
||||
currentUser,
|
||||
signOut,
|
||||
}),
|
||||
[authUser, signIn, currentUser],
|
||||
[authUser, signIn, signOut],
|
||||
)
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={ctxValue}>{children}</AuthContext.Provider>
|
||||
<AuthContext.Provider value={authContext}>{children}</AuthContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<ul>
|
||||
<li>
|
||||
<Link to="/orders">Pagamentos</Link>
|
||||
<Link to="/users">Usuários</Link>
|
||||
<Link to="/enrollments">Matrículas</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<nav>
|
||||
<ul className="flex gap-1">
|
||||
<li>
|
||||
<Link to="/orders">Pagamentos</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/users">Usuários</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/enrollments">Matrículas</Link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
signOut()
|
||||
navigate('/auth')
|
||||
}}
|
||||
>
|
||||
Sair
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{isNavigating ? <>Loading...</> : <Outlet />}
|
||||
</>
|
||||
|
||||
@@ -49,3 +49,17 @@ export default function Auth() {
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function Card({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Container>
|
||||
<div className="space-y-2.5 xl:space-y-5 rounded-xl bg-yellow-50 dark:bg-gray-700/60 p-4 lg:p-8 drop-shadow-sm shadow-sm">
|
||||
{children}
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export function Container({ children }: { children: React.ReactNode }) {
|
||||
return <div className="w-full 2xl:w-[26rem]">{children}</div>
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-100">
|
||||
<div className="w-10 h-10 border-4 border-gray-300 border-t-4 border-t-blue-500 rounded-full animate-spin" />
|
||||
|
||||
<style>{`
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
<Logo className="w-14 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" />
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<form>
|
||||
Username
|
||||
<input />
|
||||
</form>
|
||||
<Card>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Control>
|
||||
<Label>Email ou CPF</Label>
|
||||
<Input {...register('username')} />
|
||||
</Control>
|
||||
<Control>
|
||||
<Label>Senha</Label>
|
||||
<Input type="password" {...register('password')} />
|
||||
</Control>
|
||||
|
||||
<Button type="submit" isLoading={formState.isSubmitting}>
|
||||
Entrar
|
||||
</Button>
|
||||
</form>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
65
dashboard_js/package-lock.json
generated
65
dashboard_js/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user