diff --git a/apps/id.saladeaula.digital/app/entry.server.tsx b/apps/id.saladeaula.digital/app/entry.server.tsx index bb1ec05..4a11431 100644 --- a/apps/id.saladeaula.digital/app/entry.server.tsx +++ b/apps/id.saladeaula.digital/app/entry.server.tsx @@ -41,3 +41,13 @@ export default async function handleRequest( status: responseStatusCode }) } + +// https://reactrouter.com/how-to/suspense#timeouts +export const streamTimeout = 7_000 + +// https://reactrouter.com/how-to/error-reporting +export const handleError: HandleErrorFunction = (error, { request }) => { + if (!request.signal.aborted) { + console.error(error) + } +} diff --git a/apps/id.saladeaula.digital/app/routes.ts b/apps/id.saladeaula.digital/app/routes.ts index cedc2be..4a2f24d 100644 --- a/apps/id.saladeaula.digital/app/routes.ts +++ b/apps/id.saladeaula.digital/app/routes.ts @@ -8,7 +8,7 @@ import { export default [ layout('routes/layout.tsx', [ index('routes/index.tsx'), - route('/reset/:code', 'routes/reset.tsx'), + route('/reset/:token', 'routes/reset.tsx'), route('/forgot', 'routes/forgot.tsx'), route('/deny', 'routes/deny.tsx'), layout('routes/register/layout.tsx', [ diff --git a/apps/id.saladeaula.digital/app/routes/authorize.ts b/apps/id.saladeaula.digital/app/routes/authorize.ts index c1e57c4..b0e3190 100644 --- a/apps/id.saladeaula.digital/app/routes/authorize.ts +++ b/apps/id.saladeaula.digital/app/routes/authorize.ts @@ -1,4 +1,4 @@ -import type { Route } from './+types' +import type { Route } from './+types/authorize' import { redirect } from 'react-router' import { parse } from 'cookie' @@ -7,8 +7,8 @@ export async function loader({ request, context }: Route.LoaderArgs) { const cookies = parse(request.headers.get('Cookie') || '') const url = new URL(request.url) const loginUrl = new URL('/', url.origin) - const issuerUrl = new URL('/authorize', context.cloudflare.env.ISSUER_URL) - issuerUrl.search = url.search + const authorizeUrl = new URL('/authorize', context.cloudflare.env.ISSUER_URL) + authorizeUrl.search = url.search loginUrl.search = url.search if (!cookies?.SID) { @@ -19,7 +19,7 @@ export async function loader({ request, context }: Route.LoaderArgs) { throw redirect(context.cloudflare.env.APP_URL) } - const r = await fetch(issuerUrl.toString(), { + const r = await fetch(authorizeUrl.toString(), { method: 'GET', headers: new Headers([ ['Content-Type', 'application/json'], diff --git a/apps/id.saladeaula.digital/app/routes/deny.tsx b/apps/id.saladeaula.digital/app/routes/deny.tsx index 03d61d1..79bd033 100644 --- a/apps/id.saladeaula.digital/app/routes/deny.tsx +++ b/apps/id.saladeaula.digital/app/routes/deny.tsx @@ -1,4 +1,4 @@ -import type { Route } from './+types' +import type { Route } from './+types/deny' import { LockIcon } from 'lucide-react' diff --git a/apps/id.saladeaula.digital/app/routes/forgot.tsx b/apps/id.saladeaula.digital/app/routes/forgot.tsx index 98e376c..0a0b178 100644 --- a/apps/id.saladeaula.digital/app/routes/forgot.tsx +++ b/apps/id.saladeaula.digital/app/routes/forgot.tsx @@ -1,9 +1,10 @@ -import type { Route } from './+types' +import type { Route } from './+types/forgot' import { isValidCPF } from '@brazilian-utils/brazilian-utils' import { zodResolver } from '@hookform/resolvers/zod' import { useForm } from 'react-hook-form' import { Link } from 'react-router' +import { MailIcon } from 'lucide-react' import { z } from 'zod' import logo from '@repo/ui/components/logo2.svg' @@ -17,6 +18,10 @@ import { FormMessage } from '@repo/ui/components/ui/form' import { Input } from '@repo/ui/components/ui/input' +import { useFetcher } from 'react-router' +import { Spinner } from '@repo/ui/components/ui/spinner' +import { request as req } from '@repo/util/request' +import { useEffect } from 'react' const schema = z.object({ username: z @@ -38,14 +43,67 @@ export function meta({}: Route.MetaArgs) { return [{ title: 'Redefinir senha · EDUSEG®' }] } -export default function Forgot({}: Route.ComponentProps) { - const form = useForm({ - resolver: zodResolver(schema) +export async function action({ request, context }: Route.ActionArgs) { + const url = new URL('/forgot', context.cloudflare.env.ISSUER_URL) + const body = await request.json() + + console.log(url.toString()) + + const r = await fetch(url.toString(), { + method: 'POST', + headers: new Headers({ 'Content-Type': 'application/json' }), + body: JSON.stringify(body), + signal: request.signal }) - const { control, handleSubmit, formState } = form + const data = (await r.json()) as any + + if (r.ok) { + return { ok: true, ...data } + } + + return { ok: false, ...data } +} + +export default function Forgot({}: Route.ComponentProps) { + const fetcher = useFetcher() + const form = useForm({ resolver: zodResolver(schema) }) + const { control, handleSubmit, formState, setError } = form const onSubmit = async (data: Schema) => { - console.log(data) + await fetcher.submit(data, { + method: 'POST', + encType: 'application/json' + }) + } + + useEffect(() => { + const message = fetcher.data?.message + + switch (message) { + case 'User not found': + return setError('username', { + message: + 'Não encontramos sua conta. Verifique se está usando o Email ou CPF correto', + type: 'manual' + }) + } + }, [fetcher.data, setError]) + + if (fetcher.data?.ok) { + return ( +
+ +
+

+ Verifique seu email +

+

+ Acabamos de enviar um email com as instruções para{' '} + {fetcher.data?.email} +

+
+
+ ) } return ( @@ -68,7 +126,7 @@ export default function Forgot({}: Route.ComponentProps) {
- + + {formState.isSubmitting && } Enviar instruções diff --git a/apps/id.saladeaula.digital/app/routes/index.tsx b/apps/id.saladeaula.digital/app/routes/index.tsx index 60da65e..f22b9ba 100644 --- a/apps/id.saladeaula.digital/app/routes/index.tsx +++ b/apps/id.saladeaula.digital/app/routes/index.tsx @@ -47,14 +47,11 @@ export function meta({}: Route.MetaArgs) { } export async function action({ request, context }: Route.ActionArgs) { - const issuerUrl = new URL( - '/authentication', - context.cloudflare.env.ISSUER_URL - ) + const url = new URL('/authentication', context.cloudflare.env.ISSUER_URL) const formData = Object.fromEntries(await request.formData()) try { - const r = await fetch(issuerUrl.toString(), { + const r = await fetch(url.toString(), { method: 'POST', headers: new Headers({ 'Content-Type': 'application/json' }), body: JSON.stringify(formData) diff --git a/apps/id.saladeaula.digital/app/routes/register/cpf.tsx b/apps/id.saladeaula.digital/app/routes/register/cpf.tsx index d3e36d5..116a890 100644 --- a/apps/id.saladeaula.digital/app/routes/register/cpf.tsx +++ b/apps/id.saladeaula.digital/app/routes/register/cpf.tsx @@ -114,15 +114,8 @@ export function Cpf() { )} /> - diff --git a/apps/id.saladeaula.digital/app/routes/register/index.tsx b/apps/id.saladeaula.digital/app/routes/register/index.tsx index 7cdbe11..80cd4c7 100644 --- a/apps/id.saladeaula.digital/app/routes/register/index.tsx +++ b/apps/id.saladeaula.digital/app/routes/register/index.tsx @@ -1,4 +1,4 @@ -import type { Route } from '../+types' +import type { Route } from './+types/index' import { PatternFormat } from 'react-number-format' import { zodResolver } from '@hookform/resolvers/zod' @@ -34,10 +34,10 @@ export function meta({}: Route.MetaArgs) { } export async function action({ request, context }: Route.ActionArgs) { - const issuerUrl = new URL('/register', context.cloudflare.env.ISSUER_URL) + const url = new URL('/register', context.cloudflare.env.ISSUER_URL) const body = await request.json() - const r = await fetch(issuerUrl.toString(), { + const r = await fetch(url.toString(), { method: 'POST', headers: new Headers({ 'Content-Type': 'application/json' }), body: JSON.stringify(body), @@ -62,7 +62,7 @@ export default function Signup({}: Route.ComponentProps) { const onSubmit = async (data: Schema) => { await fetcher.submit(JSON.stringify({ ...user, ...data }), { - method: 'post', + method: 'POST', encType: 'application/json' }) } @@ -81,7 +81,7 @@ export default function Signup({}: Route.ComponentProps) { {user ? (
- + {user?.never_logged && ( @@ -197,14 +197,10 @@ export default function Signup({}: Route.ComponentProps) { diff --git a/apps/id.saladeaula.digital/app/routes/register/layout.tsx b/apps/id.saladeaula.digital/app/routes/register/layout.tsx index 7fee5bc..a306ba6 100644 --- a/apps/id.saladeaula.digital/app/routes/register/layout.tsx +++ b/apps/id.saladeaula.digital/app/routes/register/layout.tsx @@ -1,4 +1,4 @@ -import type { Route } from './+types/index' +import type { Route } from './+types/layout' import { Outlet, Link } from 'react-router' diff --git a/apps/id.saladeaula.digital/app/routes/reset.tsx b/apps/id.saladeaula.digital/app/routes/reset.tsx new file mode 100644 index 0000000..d681002 --- /dev/null +++ b/apps/id.saladeaula.digital/app/routes/reset.tsx @@ -0,0 +1,157 @@ +import type { Route } from './+types/reset' + +import { useState } from 'react' +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { useFetcher, redirect } from 'react-router' +import { z } from 'zod' + +import logo from '@repo/ui/components/logo2.svg' +import { Input } from '@repo/ui/components/ui/input' +import { Button } from '@repo/ui/components/ui/button' +import { Checkbox } from '@repo/ui/components/ui/checkbox' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@repo/ui/components/ui/form' +import { Label } from '@repo/ui/components/ui/label' +import { Spinner } from '@repo/ui/components/ui/spinner' +import { request as req } from '@repo/util/request' + +export const formSchema = z + .object({ + password: z + .string() + .nonempty('Digite sua senha') + .min(6, 'Deve ter no mínimo 6 caracteres'), + confirm_password: z.string() + }) + .refine((data) => data.password === data.confirm_password, { + message: 'As senhas não coincidem', + path: ['confirm_password'] + }) + +export type Schema = z.infer + +export function meta({}: Route.MetaArgs) { + return [{ title: 'Define sua nova senha · EDUSEG®' }] +} + +export async function action({ params, request, context }: Route.ActionArgs) { + const { token } = params + const url = new URL(`/reset/${token}`, context.cloudflare.env.ISSUER_URL) + const body = await request.json() + + console.log(url.toString()) + + // const r = await fetch(issuerUrl.toString(), { + // method: 'POST', + // headers: new Headers({ 'Content-Type': 'application/json' }), + // body: JSON.stringify(body), + // signal: request.signal + // }) + + // if (r.ok) { + // throw redirect('/authorize', { headers: r.headers }) + // } + + // return { ok: false, error: await r.json() } + + await new Promise((r) => setTimeout(r, 2000)) + return { ok: true } +} + +export default function Route({}: Route.ComponentProps) { + const fetcher = useFetcher() + const [show, setShow] = useState(false) + const form = useForm({ resolver: zodResolver(formSchema) }) + const { handleSubmit, control, formState } = form + + const onSubmit = async ({ password }: Schema) => { + await fetcher.submit(JSON.stringify({ new_password: password }), { + method: 'POST', + encType: 'application/json' + }) + } + + return ( +
+
+
+ EDUSEG® +
+
+ +
+

+ Defina sua nova senha +

+

+ Defina uma nova senha para manter sua conta sempre segura. +

+
+
+ + ( + + Senha + + + + + + )} + /> + + ( + + Confirmar senha + + + +
+ setShow((x) => !x)} + tabIndex={-1} + /> + +
+ +
+ )} + /> + + + + +
+ ) +} diff --git a/apps/id.saladeaula.digital/app/routes/upstream.ts b/apps/id.saladeaula.digital/app/routes/upstream.ts index baf6a71..e616df5 100644 --- a/apps/id.saladeaula.digital/app/routes/upstream.ts +++ b/apps/id.saladeaula.digital/app/routes/upstream.ts @@ -1,4 +1,4 @@ -import type { Route } from './+types' +import type { Route } from './+types/upstream' export const loader = proxy export const action = proxy diff --git a/id.saladeaula.digital/app/routes/forgot.py b/id.saladeaula.digital/app/routes/forgot.py index 29f5fc0..3858b16 100644 --- a/id.saladeaula.digital/app/routes/forgot.py +++ b/id.saladeaula.digital/app/routes/forgot.py @@ -3,10 +3,10 @@ from http import HTTPStatus from typing import Annotated from uuid import uuid4 +from aws_lambda_powertools.event_handler import content_types from aws_lambda_powertools.event_handler.api_gateway import Response, Router from aws_lambda_powertools.event_handler.exceptions import NotFoundError from aws_lambda_powertools.event_handler.openapi.params import Body -from aws_lambda_powertools.utilities.data_masking import DataMasking from layercake.dateutils import now, ttl from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey from layercake.extra_types import CpfStr @@ -18,22 +18,17 @@ from config import USER_TABLE router = Router() dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) -data_masker = DataMasking() - -masking_rules = { - 'email': {'regex_pattern': '(.)(.*)(..)(@.*)', 'mask_format': r'\1****\3\4'}, -} class UserNotFoundError(NotFoundError): ... -@router.post('/forgot') +@router.post('/forgot', compress=True) def forgot(username: Annotated[EmailStr | CpfStr, Body(embed=True)]): now_ = now() user = _get_user(username) reset_ttl = ttl(start_dt=now_, hours=3) - code = uuid4() + code = str(uuid4()) with dyn.transact_writer() as transact: transact.update( @@ -63,6 +58,7 @@ def forgot(username: Annotated[EmailStr | CpfStr, Body(embed=True)]): 'id': 'PASSWORD_RESET', 'sk': f'CODE#{code}', 'name': user.name, + 'email': user.email, 'user_id': user.id, 'ttl': reset_ttl, 'created_at': now_, @@ -70,13 +66,11 @@ def forgot(username: Annotated[EmailStr | CpfStr, Body(embed=True)]): ) return Response( + content_type=content_types.APPLICATION_JSON, status_code=HTTPStatus.CREATED, - body=data_masker.erase( - { - 'email': user.email, - }, - masking_rules=masking_rules, - ), + body={ + 'email': mask_email(user.email), + }, ) @@ -114,3 +108,9 @@ def _get_user(username: str) -> User: return User( **pick(('id', 'name', 'email'), user), ) + + +def mask_email(email): + username, domain = email.split('@') + username = username[0] + '*' * (len(username) - 3) + username[-2:] + return f'{username}@{domain}' diff --git a/id.saladeaula.digital/app/routes/register.py b/id.saladeaula.digital/app/routes/register.py index 6212d4f..2125d5a 100644 --- a/id.saladeaula.digital/app/routes/register.py +++ b/id.saladeaula.digital/app/routes/register.py @@ -7,7 +7,6 @@ from aws_lambda_powertools.event_handler import content_types from aws_lambda_powertools.event_handler.api_gateway import Response, Router from aws_lambda_powertools.event_handler.exceptions import ServiceError from aws_lambda_powertools.event_handler.openapi.params import Body -from aws_lambda_powertools.shared.cookies import Cookie from layercake.dateutils import now, ttl from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair from layercake.extra_types import CpfStr, NameStr diff --git a/id.saladeaula.digital/tests/routes/test_forgot.py b/id.saladeaula.digital/tests/routes/test_forgot.py index af9ab15..699a7d9 100644 --- a/id.saladeaula.digital/tests/routes/test_forgot.py +++ b/id.saladeaula.digital/tests/routes/test_forgot.py @@ -1,3 +1,4 @@ +import json from http import HTTPMethod from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey @@ -20,7 +21,8 @@ def test_forgot( ), lambda_context, ) - assert 's****io@somosbeta.com.br' == r['body']['email'] + body = json.loads(r['body']) + assert 's***io@somosbeta.com.br' == body['email'] app.lambda_handler( http_api_proxy( @@ -34,4 +36,4 @@ def test_forgot( forgot = dynamodb_persistence_layer.collection.query( PartitionKey('PASSWORD_RESET'), ) - assert len(forgot['items']) == 3 + assert len(forgot['items']) == 5