finish register

This commit is contained in:
2025-12-03 16:27:07 -03:00
parent 967e275f29
commit 392dccebc1
11 changed files with 90 additions and 44 deletions

View File

@@ -58,6 +58,8 @@ def health():
@app.exception_handler(ServiceError) @app.exception_handler(ServiceError)
def exc_error(exc: ServiceError): def exc_error(exc: ServiceError):
logger.exception(exc)
return JSONResponse( return JSONResponse(
body={ body={
'type': type(exc).__name__, 'type': type(exc).__name__,

View File

@@ -93,6 +93,7 @@ def add(
# Post-migration (users): rename `email` to `EMAIL` # Post-migration (users): rename `email` to `EMAIL`
'id': 'email', 'id': 'email',
'sk': email, 'sk': email,
'user_id': user_id,
'created_at': now_, 'created_at': now_,
}, },
# Prevent duplicate emails # Prevent duplicate emails

View File

@@ -94,24 +94,22 @@ export default function Index({}: Route.ComponentProps) {
} }
useEffect(() => { useEffect(() => {
if (fetcher.state === 'idle' && fetcher.data) { const message = fetcher.data?.message
const message = fetcher.data?.message
switch (message) { switch (message) {
case 'User not found': case 'User not found':
return setError('username', { return setError('username', {
message: message:
'Não encontramos sua conta. Verifique se está usando o Email ou CPF correto', 'Não encontramos sua conta. Verifique se está usando o Email ou CPF correto',
type: 'manual' type: 'manual'
}) })
case 'Invalid credentials': case 'Invalid credentials':
return setError('password', { return setError('password', {
message: 'A senha está incorreta', message: 'A senha está incorreta',
type: 'manual' type: 'manual'
}) })
}
} }
}, [fetcher.state, fetcher.data]) }, [fetcher.data])
return ( return (
<> <>

View File

@@ -1,11 +1,11 @@
import type { Route } from '../+types' import type { Route } from '../+types'
import { useRequest } from 'ahooks'
import { PatternFormat } from 'react-number-format' import { PatternFormat } from 'react-number-format'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { useState } from 'react' import { useState } from 'react'
import { CheckCircle2Icon } from 'lucide-react' import { CheckCircle2Icon } from 'lucide-react'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { redirect, useFetcher } from 'react-router'
import { Button } from '@repo/ui/components/ui/button' import { Button } from '@repo/ui/components/ui/button'
import { Checkbox } from '@repo/ui/components/ui/checkbox' import { Checkbox } from '@repo/ui/components/ui/checkbox'
@@ -19,14 +19,15 @@ import {
} from '@repo/ui/components/ui/form' } from '@repo/ui/components/ui/form'
import { Input } from '@repo/ui/components/ui/input' import { Input } from '@repo/ui/components/ui/input'
import { Label } from '@repo/ui/components/ui/label' import { Label } from '@repo/ui/components/ui/label'
import { Cpf } from './cpf'
import { formSchema, type Schema, RegisterContext, type User } from './data'
import { import {
Alert, Alert,
AlertDescription, AlertDescription,
AlertTitle AlertTitle
} from '@repo/ui/components/ui/alert' } from '@repo/ui/components/ui/alert'
import { Spinner } from '@repo/ui/components/ui/spinner'
import { Cpf } from './cpf'
import { formSchema, type Schema, RegisterContext, type User } from './data'
export function meta({}: Route.MetaArgs) { export function meta({}: Route.MetaArgs) {
return [{ title: 'Criar conta · EDUSEG®' }] return [{ title: 'Criar conta · EDUSEG®' }]
@@ -39,32 +40,27 @@ export async function action({ request, context }: Route.ActionArgs) {
const r = await fetch(issuerUrl.toString(), { const r = await fetch(issuerUrl.toString(), {
method: 'POST', method: 'POST',
headers: new Headers({ 'Content-Type': 'application/json' }), headers: new Headers({ 'Content-Type': 'application/json' }),
body: JSON.stringify(body) body: JSON.stringify(body),
signal: request.signal
}) })
console.log(await r.json()) throw redirect('/authorize', { headers: r.headers })
} }
export default function Signup({}: Route.ComponentProps) { export default function Signup({}: Route.ComponentProps) {
const fetcher = useFetcher()
const [show, setShow] = useState(false) const [show, setShow] = useState(false)
const [user, setUser] = useState<User | null>(null) const [user, setUser] = useState<User | null>(null)
const form = useForm({ const form = useForm({
resolver: zodResolver(formSchema) resolver: zodResolver(formSchema)
}) })
const { control, handleSubmit, formState } = form const { control, handleSubmit, formState, setError } = form
const { runAsync } = useRequest(
async (user) => {
return await fetch(`/register`, {
method: 'POST',
headers: new Headers({ 'Content-Type': 'application/json' }),
body: JSON.stringify(user)
})
},
{ manual: true }
)
const onSubmit = async (data: Schema) => { const onSubmit = async (data: Schema) => {
await runAsync({ ...user, ...data }) await fetcher.submit(JSON.stringify({ ...user, ...data }), {
method: 'post',
encType: 'application/json'
})
} }
return ( return (
@@ -187,9 +183,14 @@ export default function Signup({}: Route.ComponentProps) {
<Button <Button
type="submit" type="submit"
className="w-full cursor-pointer" className="w-full cursor-pointer relative overflow-hidden"
disabled={formState.isSubmitting} disabled={formState.isSubmitting}
> >
{formState.isSubmitting && (
<div className="absolute bg-lime-500 inset-0 flex items-center justify-center">
<Spinner />
</div>
)}
Criar conta Criar conta
</Button> </Button>
</form> </form>

View File

@@ -44,6 +44,8 @@ def health():
@app.exception_handler(ServiceError) @app.exception_handler(ServiceError)
def exc_error(exc: ServiceError): def exc_error(exc: ServiceError):
logger.exception(exc)
return Response( return Response(
body={ body={
'type': type(exc).__name__, 'type': type(exc).__name__,

View File

@@ -4,9 +4,11 @@ from typing import TYPE_CHECKING
import boto3 import boto3
if TYPE_CHECKING: if TYPE_CHECKING:
from mypy_boto3_cognito_idp import CognitoIdentityProviderClient
from mypy_boto3_dynamodb.client import DynamoDBClient from mypy_boto3_dynamodb.client import DynamoDBClient
else: else:
DynamoDBClient = object DynamoDBClient = object
CognitoIdentityProviderClient = object
def get_dynamodb_client() -> DynamoDBClient: def get_dynamodb_client() -> DynamoDBClient:
@@ -17,3 +19,4 @@ def get_dynamodb_client() -> DynamoDBClient:
dynamodb_client: DynamoDBClient = get_dynamodb_client() dynamodb_client: DynamoDBClient = get_dynamodb_client()
idp_client: CognitoIdentityProviderClient = boto3.client('cognito-idp')

View File

@@ -2,7 +2,6 @@ from http import HTTPStatus
from typing import Annotated from typing import Annotated
from uuid import uuid4 from uuid import uuid4
import boto3
from aws_lambda_powertools.event_handler import ( from aws_lambda_powertools.event_handler import (
Response, Response,
) )
@@ -17,7 +16,7 @@ from layercake.dateutils import now, ttl
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
from passlib.hash import pbkdf2_sha256 from passlib.hash import pbkdf2_sha256
from boto3clients import dynamodb_client from boto3clients import dynamodb_client, idp_client
from config import ( from config import (
OAUTH2_TABLE, OAUTH2_TABLE,
SESSION_EXPIRES_IN, SESSION_EXPIRES_IN,
@@ -25,7 +24,6 @@ from config import (
router = Router() router = Router()
dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client) dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
idp = boto3.client('cognito-idp')
class InvalidCredentialsError(UnauthorizedError): ... class InvalidCredentialsError(UnauthorizedError): ...
@@ -125,7 +123,7 @@ def _get_idp_user(
).digest() ).digest()
try: try:
idp.initiate_auth( idp_client.initiate_auth(
AuthFlow='USER_PASSWORD_AUTH', AuthFlow='USER_PASSWORD_AUTH',
AuthParameters={ AuthParameters={
'USERNAME': username, 'USERNAME': username,
@@ -155,7 +153,9 @@ def new_session(user_id: str) -> str:
exp = ttl(start_dt=now_, seconds=SESSION_EXPIRES_IN) exp = ttl(start_dt=now_, seconds=SESSION_EXPIRES_IN)
with dyn.transact_writer() as transact: with dyn.transact_writer() as transact:
transact.delete(key=KeyPair(user_id, 'FAILED_ATTEMPTS')) transact.delete(
key=KeyPair(user_id, 'FAILED_ATTEMPTS'),
)
transact.update( transact.update(
key=KeyPair(user_id, '0'), key=KeyPair(user_id, '0'),
# Post-migration (users): uncomment the following line # Post-migration (users): uncomment the following line

View File

@@ -3,9 +3,11 @@ from http import HTTPStatus
from typing import Annotated from typing import Annotated
from uuid import uuid4 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.api_gateway import Response, Router
from aws_lambda_powertools.event_handler.exceptions import NotFoundError, ServiceError from aws_lambda_powertools.event_handler.exceptions import NotFoundError, ServiceError
from aws_lambda_powertools.event_handler.openapi.params import Body 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.dateutils import now, ttl
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
from layercake.extra_types import CpfStr, NameStr from layercake.extra_types import CpfStr, NameStr
@@ -14,7 +16,9 @@ from passlib.hash import pbkdf2_sha256
from pydantic import UUID4, EmailStr from pydantic import UUID4, EmailStr
from boto3clients import dynamodb_client from boto3clients import dynamodb_client
from config import OAUTH2_TABLE from config import OAUTH2_TABLE, SESSION_EXPIRES_IN
from .authentication import new_session
router = Router() router = Router()
dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client) dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
@@ -68,15 +72,36 @@ def register(
) )
return Response( return Response(
content_type=content_types.APPLICATION_JSON,
status_code=HTTPStatus.OK, status_code=HTTPStatus.OK,
compress=True,
body=asdict(new_user), body=asdict(new_user),
cookies=[
_cookie(existing['id']),
],
) )
_create_user(user=new_user, password=password) _create_user(user=new_user, password=password)
return Response( return Response(
content_type=content_types.APPLICATION_JSON,
status_code=HTTPStatus.CREATED, status_code=HTTPStatus.CREATED,
compress=True,
body=asdict(new_user), body=asdict(new_user),
cookies=[
_cookie(new_user.id),
],
)
def _cookie(user_id: str) -> Cookie:
return Cookie(
name='SID',
value=new_session(user_id),
http_only=True,
secure=True,
same_site=None,
max_age=SESSION_EXPIRES_IN,
) )

View File

@@ -12,7 +12,7 @@ dev = [
"pytest>=8.4.1", "pytest>=8.4.1",
"ruff>=0.12.1", "ruff>=0.12.1",
"pytest-cov>=6.2.1", "pytest-cov>=6.2.1",
"boto3-stubs[essential]>=1.40.44", "boto3-stubs[cognito-idp,essential]>=1.40.44",
] ]

View File

@@ -23,6 +23,7 @@ def test_preexisting_user(
'cpf': '07879819908', 'cpf': '07879819908',
'password': 'Led@Zepellin', 'password': 'Led@Zepellin',
'email': 'sergio@somosbeta.com.br', 'email': 'sergio@somosbeta.com.br',
'never_logged': 'true',
}, },
), ),
lambda_context, lambda_context,
@@ -80,6 +81,7 @@ def test_preexisting_user_update_email(
lambda_context, lambda_context,
) )
assert r['statusCode'] == HTTPStatus.OK assert r['statusCode'] == HTTPStatus.OK
assert 'cookies' in r
user = dynamodb_persistence_layer.collection.get_items( user = dynamodb_persistence_layer.collection.get_items(
TransactKey( TransactKey(

View File

@@ -133,6 +133,9 @@ wheels = [
] ]
[package.optional-dependencies] [package.optional-dependencies]
cognito-idp = [
{ name = "mypy-boto3-cognito-idp" },
]
essential = [ essential = [
{ name = "mypy-boto3-cloudformation" }, { name = "mypy-boto3-cloudformation" },
{ name = "mypy-boto3-dynamodb" }, { name = "mypy-boto3-dynamodb" },
@@ -423,7 +426,7 @@ dependencies = [
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "boto3-stubs", extra = ["essential"] }, { name = "boto3-stubs", extra = ["cognito-idp", "essential"] },
{ name = "jsonlines" }, { name = "jsonlines" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-cov" }, { name = "pytest-cov" },
@@ -435,7 +438,7 @@ requires-dist = [{ name = "layercake", directory = "../layercake" }]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "boto3-stubs", extras = ["essential"], specifier = ">=1.40.44" }, { name = "boto3-stubs", extras = ["cognito-idp", "essential"], specifier = ">=1.40.44" },
{ name = "jsonlines", specifier = ">=4.0.0" }, { name = "jsonlines", specifier = ">=4.0.0" },
{ name = "pytest", specifier = ">=8.4.1" }, { name = "pytest", specifier = ">=8.4.1" },
{ name = "pytest-cov", specifier = ">=6.2.1" }, { name = "pytest-cov", specifier = ">=6.2.1" },
@@ -591,6 +594,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/38/12301080cc5004593b8593c0cc7404c13d702ac1c15d4e0ccfacd1f4f416/mypy_boto3_cloudformation-1.40.44-py3-none-any.whl", hash = "sha256:64c8fe58ab7661fbb0bdea07c7375d3ebc3875760140feb6ad8f591a08a22647", size = 69896, upload-time = "2025-10-02T20:31:56.896Z" }, { url = "https://files.pythonhosted.org/packages/2e/38/12301080cc5004593b8593c0cc7404c13d702ac1c15d4e0ccfacd1f4f416/mypy_boto3_cloudformation-1.40.44-py3-none-any.whl", hash = "sha256:64c8fe58ab7661fbb0bdea07c7375d3ebc3875760140feb6ad8f591a08a22647", size = 69896, upload-time = "2025-10-02T20:31:56.896Z" },
] ]
[[package]]
name = "mypy-boto3-cognito-idp"
version = "1.40.60"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b3/4e/57aeebe4c57a6f3345cffab1c7ae05756c08fef335bc72ce34308a534835/mypy_boto3_cognito_idp-1.40.60.tar.gz", hash = "sha256:4e06656f3954e193e4dff69042bc214db5ef575ebbb606a1b5fcb6dc3d6fd32e", size = 52148, upload-time = "2025-10-27T19:43:24.788Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/d2/b42e487058fea7b43086b62150826a3aa2777f9a7fe0c9ed31e03bb05100/mypy_boto3_cognito_idp-1.40.60-py3-none-any.whl", hash = "sha256:afdb6c81676442d76be4c4bb63378758e13b970f9b366290ac2d1bfeda26f669", size = 57972, upload-time = "2025-10-27T19:43:22.069Z" },
]
[[package]] [[package]]
name = "mypy-boto3-dynamodb" name = "mypy-boto3-dynamodb"
version = "1.40.44" version = "1.40.44"