diff --git a/api.saladeaula.digital/app/routes/orgs/users/__init__.py b/api.saladeaula.digital/app/routes/orgs/users/__init__.py index 265c882..875779d 100644 --- a/api.saladeaula.digital/app/routes/orgs/users/__init__.py +++ b/api.saladeaula.digital/app/routes/orgs/users/__init__.py @@ -139,7 +139,6 @@ def _create_user(user: User, org: Org) -> bool: 'fresh_user': True, 'name': user.name, 'email': user.email, - 'email_primary': True, 'org_name': org.name, 'ttl': ttl(start_dt=now_, days=30), 'created_at': now_, diff --git a/api.saladeaula.digital/app/routes/users/emails.py b/api.saladeaula.digital/app/routes/users/emails.py index fe1f03d..bc329ee 100644 --- a/api.saladeaula.digital/app/routes/users/emails.py +++ b/api.saladeaula.digital/app/routes/users/emails.py @@ -8,8 +8,7 @@ from aws_lambda_powertools.event_handler.exceptions import ( ) from aws_lambda_powertools.event_handler.openapi.params import Body, Path, Query from layercake.dateutils import now, ttl -from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey -from layercake.funcs import pick +from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey, TransactKey from pydantic import EmailStr from typing_extensions import Annotated @@ -33,6 +32,9 @@ def get_emails(user_id: str, start_key: Annotated[str | None, Query] = None): class UserNotFoundError(NotFoundError): ... +class EmailNotFoundError(NotFoundError): ... + + class EmailConflictError(ServiceError): def __init__(self, msg: str | dict): super().__init__(HTTPStatus.CONFLICT, msg) @@ -82,7 +84,6 @@ def add( 'sk': f'EMAIL_VERIFICATION#{uuid4()}', 'name': name, 'email': email, - 'user_id': user_id, 'ttl': ttl(start_dt=now_, days=30), 'created_at': now_, } @@ -110,7 +111,6 @@ def request_verification( 'sk': f'EMAIL_VERIFICATION#{uuid4()}', 'name': name, 'email': email, - 'user_id': user_id, 'ttl': ttl(start_dt=now_, days=30), 'created_at': now_, } @@ -121,20 +121,31 @@ def request_verification( class EmailVerificationNotFoundError(NotFoundError): ... -@router.post('//emails//verify') -def verify(user_id: str, hash: str): - verification = dyn.collection.get_item( - KeyPair( - pk=user_id, - sk=f'EMAIL_VERIFICATION#{hash}', +@router.post('//emails//verify') +def verify(user_id: str, code: str): + r = dyn.collection.get_items( + TransactKey(user_id) + + SortKey( + sk='0', + rename_key='email_primary', + path_spec='email', + ) + + SortKey( + sk=f'EMAIL_VERIFICATION#{code}', + rename_key='email', + path_spec='email', ), - exc_cls=EmailVerificationNotFoundError, + flatten_top=False, ) - email, primary = pick(('email', 'email_primary'), verification, default=False) + + if 'email' not in r: + raise EmailVerificationNotFoundError('Verification code not found') + + email, email_primary = r['email'], r['email_primary'] with dyn.transact_writer() as transact: transact.delete( - key=KeyPair(user_id, f'EMAIL_VERIFICATION#{hash}'), + key=KeyPair(user_id, f'EMAIL_VERIFICATION#{code}'), ) transact.update( # Post-migration (users): rename `emails` to `EMAIL` @@ -144,15 +155,16 @@ def verify(user_id: str, hash: str): ':true': True, ':now': now(), }, + cond_expr='attribute_exists(sk)', + exc_cls=EmailNotFoundError, ) - if primary: + if email == email_primary: transact.update( key=KeyPair(user_id, '0'), update_expr='SET email_verified = :true, \ updated_at = :now', expr_attr_values={ - ':email': email, ':true': True, ':now': now(), }, diff --git a/api.saladeaula.digital/tests/routes/users/test_emails.py b/api.saladeaula.digital/tests/routes/users/test_emails.py index 2943496..e9e5c64 100644 --- a/api.saladeaula.digital/tests/routes/users/test_emails.py +++ b/api.saladeaula.digital/tests/routes/users/test_emails.py @@ -76,6 +76,40 @@ def test_email_as_primary( assert r['statusCode'] == HTTPStatus.NO_CONTENT +def test_verify_email( + app, + seeds, + http_api_proxy: HttpApiProxy, + lambda_context: LambdaContext, +): + r = app.lambda_handler( + http_api_proxy( + raw_path='/users/15bacf02-1535-4bee-9022-19d106fd7518/emails/0d29c753-55f8-42d2-908b-e4976aafc183/verify', + method=HTTPMethod.POST, + ), + lambda_context, + ) + + assert r['statusCode'] == HTTPStatus.NO_CONTENT + + +def test_verify_email_notfound( + app, + seeds, + http_api_proxy: HttpApiProxy, + lambda_context: LambdaContext, +): + r = app.lambda_handler( + http_api_proxy( + raw_path='/users/15bacf02-1535-4bee-9022-19d106fd7518/emails/abc/verify', + method=HTTPMethod.POST, + ), + lambda_context, + ) + + assert r['statusCode'] == HTTPStatus.NOT_FOUND + + def test_remove_emal( app, seeds, diff --git a/api.saladeaula.digital/tests/seeds.jsonl b/api.saladeaula.digital/tests/seeds.jsonl index d48b328..c6fb1a5 100644 --- a/api.saladeaula.digital/tests/seeds.jsonl +++ b/api.saladeaula.digital/tests/seeds.jsonl @@ -2,7 +2,8 @@ {"id": "213a6682-2c59-4404-9189-12eec0a846d4", "sk": "orgs#f6000f79-6e5c-49a0-952f-3bda330ef278", "name": "Banco do Brasil", "cnpj": "00000000000191"} {"id": "15bacf02-1535-4bee-9022-19d106fd7518", "sk": "0", "name": "Sérgio R Siqueira", "email": "sergio@somosbeta.com.br", "cpf": "07879819908"} {"id": "15bacf02-1535-4bee-9022-19d106fd7518", "sk": "emails#sergio@somosbeta.com.br", "email_primary": true, "mx_record_exists": true} -{"id": "15bacf02-1535-4bee-9022-19d106fd7518", "sk": "emails#osergiosiqueira@gmail.com", "mx_record_exists": true} +{"id": "15bacf02-1535-4bee-9022-19d106fd7518", "sk": "emails#osergiosiqueira@gmail.com", "email_verified": false, "mx_record_exists": true} +{"id": "15bacf02-1535-4bee-9022-19d106fd7518", "sk": "EMAIL_VERIFICATION#0d29c753-55f8-42d2-908b-e4976aafc183", "email": "osergiosiqueira@gmail.com", "name": "Sérgio Rafael de Siqueira"} // User orgs {"id": "15bacf02-1535-4bee-9022-19d106fd7518", "sk": "orgs#f6000f79-6e5c-49a0-952f-3bda330ef278", "name": "Banco do Brasil", "cnpj": "00000000000191"} diff --git a/apps/saladeaula.digital/app/routes.ts b/apps/saladeaula.digital/app/routes.ts index d3e8b61..06a55f0 100644 --- a/apps/saladeaula.digital/app/routes.ts +++ b/apps/saladeaula.digital/app/routes.ts @@ -13,6 +13,7 @@ export default [ route('settings', 'routes/settings/layout.tsx', [ index('routes/settings/profile.tsx'), route('emails', 'routes/settings/emails/index.tsx'), + route('emails/:code/verify', 'routes/settings/emails/verify.tsx'), route('password', 'routes/settings/password.tsx'), route('orgs', 'routes/settings/orgs.tsx') ]), diff --git a/users-events/app/events/batch/chunks_into_users.py b/users-events/app/events/batch/chunks_into_users.py index 265c015..da34f33 100644 --- a/users-events/app/events/batch/chunks_into_users.py +++ b/users-events/app/events/batch/chunks_into_users.py @@ -155,7 +155,6 @@ def _create_user(rawuser: dict, context: dict) -> None: 'fresh_user': True, 'name': user.name, 'email': user.email, - 'email_primary': True, 'org_name': org.name, 'ttl': ttl(start_dt=now_, days=30), 'created_at': now_, diff --git a/users-events/app/events/send_verification_email.py b/users-events/app/events/send_verification_email.py index 1cea4cd..8c65c67 100644 --- a/users-events/app/events/send_verification_email.py +++ b/users-events/app/events/send_verification_email.py @@ -17,7 +17,7 @@ Oi {first_name}, tudo bem?

Para proteger sua conta na EDUSEG, precisamos apenas verificar seu endereço de email: {email}.

- + 👉 Clique aqui para verificar endereço de email """ diff --git a/users-events/app/events/send_welcome_email.py b/users-events/app/events/send_welcome_email.py index 5b7ce1a..09ef983 100644 --- a/users-events/app/events/send_welcome_email.py +++ b/users-events/app/events/send_welcome_email.py @@ -16,7 +16,7 @@ Oi {first_name}, tudo bem?

Sua conta foi criada na EDUSEG pela empresa {org_name}.

- + 👉 Clique aqui para fazer seu primeiro acesso """