add flash message

This commit is contained in:
2025-11-30 20:11:26 -03:00
parent b5f3648f44
commit 9a0272af1d
8 changed files with 111 additions and 21 deletions

View File

@@ -52,8 +52,13 @@ def add(
)
with dyn.transact_writer() as transact:
transact.condition(
transact.update(
key=KeyPair(user_id, '0'),
# Makes the email searchable
update_expr='ADD emails :email',
expr_attr_values={
':email': {email},
},
cond_expr='attribute_exists(sk)',
exc_cls=UserNotFoundError,
)
@@ -75,6 +80,7 @@ def add(
'sk': email,
'created_at': now_,
},
# Prevent duplicate emails
cond_expr='attribute_not_exists(sk)',
exc_cls=EmailConflictError,
)
@@ -172,7 +178,7 @@ def verify(user_id: str, code: str):
exc_cls=UserNotFoundError,
)
return JSONResponse(status_code=HTTPStatus.NO_CONTENT)
return JSONResponse(status_code=HTTPStatus.OK, body={'email_verified': email})
@router.patch('/<user_id>/emails/primary')
@@ -213,13 +219,11 @@ def primary(
)
transact.update(
key=KeyPair(user_id, '0'),
update_expr='DELETE emails :email_set \
SET email = :email, \
update_expr='SET email = :email, \
email_verified = :email_verified, \
updated_at = :now',
expr_attr_values={
':email': new_email,
':email_set': {new_email},
':email_verified': email_verified,
':now': now_,
},

View File

@@ -44,14 +44,16 @@ def test_add_email(
)
assert r['statusCode'] == HTTPStatus.CREATED
email_verification = dynamodb_persistence_layer.collection.query(
r = dynamodb_persistence_layer.collection.query(
KeyPair(
'15bacf02-1535-4bee-9022-19d106fd7518',
'EMAIL_VERIFICATION',
)
)
assert 'name' in email_verification['items'][0]
assert email_verification['items'][0]['email'] == 'osergiosiqueira+pytest@gmail.com'
items = r['items']
assert len(items) == 2
assert any(x.get('email') == 'osergiosiqueira+pytest@gmail.com' for x in items)
def test_email_as_primary(
@@ -59,6 +61,7 @@ def test_email_as_primary(
seeds,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
):
r = app.lambda_handler(
http_api_proxy(
@@ -75,6 +78,12 @@ def test_email_as_primary(
assert r['statusCode'] == HTTPStatus.NO_CONTENT
r = dynamodb_persistence_layer.collection.get_item(
KeyPair('15bacf02-1535-4bee-9022-19d106fd7518', '0')
)
assert r['email'] == 'osergiosiqueira@gmail.com'
assert r['emails'] == {'osergiosiqueira@gmail.com', 'sergio@somosbeta.combr'}
def test_verify_email(
app,

View File

@@ -1,6 +1,6 @@
// Users
{"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": "0", "name": "Sérgio R Siqueira", "email": "sergio@somosbeta.com.br", "emails": ["osergiosiqueira@gmail.com", "sergio@somosbeta.combr"], "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", "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"}

View File

@@ -3,6 +3,7 @@ import type { Route } from './+types/layout'
import { useToggle } from 'ahooks'
import { MenuIcon } from 'lucide-react'
import { Link, NavLink, Outlet } from 'react-router'
import { toast } from 'sonner'
import { Toaster } from '@repo/ui/components/ui/sonner'
import { userContext } from '@repo/auth/context'
@@ -23,12 +24,31 @@ import {
SheetTrigger
} from '@repo/ui/components/ui/sheet'
import type { User } from '@repo/auth/auth'
import { createSessionStorage } from '@repo/auth/session'
import { data } from 'react-router'
import { useEffect } from 'react'
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
export async function loader({ context }: Route.ActionArgs) {
export async function loader({ context, request }: Route.ActionArgs) {
const user = context.get(userContext) as User
return Response.json({ user })
const sessionStorage = createSessionStorage(context.cloudflare.env)
const session = await sessionStorage.getSession(request.headers.get('cookie'))
const flash = {
error: session.get('error'),
success: session.get('success'),
info: session.get('info')
}
return data(
{ user, flash },
{
headers: new Headers({
'Set-Cookie': await sessionStorage.commitSession(session)
})
}
)
}
const navMain = [
@@ -47,10 +67,16 @@ const navMain = [
]
export default function Component({
loaderData: { user }
loaderData: { user, flash }
}: Route.ComponentProps) {
const [isOpen, { toggle }] = useToggle()
useEffect(() => {
if (flash.error) toast.error(flash.error)
if (flash.success) toast.success(flash.success)
if (flash.info) toast.info(flash.info)
}, [flash])
return (
<div className="relative flex flex-col flex-1 min-w-0 h-full">
<header

View File

@@ -42,7 +42,6 @@ import { request as req } from '@repo/util/request'
import { Skeleton } from '@repo/ui/components/skeleton'
import { Button } from '@repo/ui/components/ui/button'
import { useOutletContext } from 'react-router'
import type { User as AuthUser } from '@repo/auth/auth'
import type { User } from '@repo/ui/routes/users/data'
import {
Item,
@@ -59,7 +58,8 @@ import { Primary } from './primary'
const ActionMenuContext = createContext<Email | null>(null)
export async function loader({ request, context }: Route.LoaderArgs) {
const user = context.get(userContext) as AuthUser
const user = context.get(userContext)
const data = req({
url: `/users/${user.sub}/emails`,
request,

View File

@@ -0,0 +1,33 @@
import type { Route } from './+types/verify'
import { redirect } from 'react-router'
import { userContext } from '@repo/auth/context'
import { createSessionStorage } from '@repo/auth/session'
import { HttpMethod, request as req } from '@repo/util/request'
export async function loader({ params, request, context }: Route.LoaderArgs) {
const { code } = params
const sessionStorage = createSessionStorage(context.cloudflare.env)
const session = await sessionStorage.getSession(request.headers.get('cookie'))
const user = context.get(userContext)
const r = await req({
url: `/users/${user.sub}/emails/${code}/verify`,
method: HttpMethod.POST,
request,
context
})
if (r.ok) {
session.flash('success', 'Seu email foi verificado.')
} else {
session.flash('info', 'O email já está verificado.')
}
return redirect('/settings/emails', {
headers: new Headers({
'Set-Cookie': await sessionStorage.commitSession(session)
})
})
}

View File

@@ -15,7 +15,7 @@ export const authMiddleware = async (
const strategy = authenticator.get<OAuth2Strategy<User>>('oidc')
const session = await sessionStorage.getSession(request.headers.get('cookie'))
const requestId = context.get(requestIdContext)
let user = session.get('user') as User | null
let user = session.get('user')
if (!user) {
console.log('There is no user logged in')
@@ -67,7 +67,11 @@ export const authMiddleware = async (
context.set(userContext, user)
const response = await next()
const sessionCookie = await sessionStorage.commitSession(session)
response.headers.set('Set-Cookie', sessionCookie)
if (!response.headers.has('Set-Cookie')) {
const sessionCookie = await sessionStorage.commitSession(session)
response.headers.set('Set-Cookie', sessionCookie)
}
return response
}

View File

@@ -1,7 +1,22 @@
import { createCookieSessionStorage } from 'react-router'
import { type User } from './auth'
export function createSessionStorage(env) {
const sessionStorage = createCookieSessionStorage({
type SessionData = {
user: User
returnTo: string
}
type SessionFlashData = {
error: string
success: string
info: string
}
export function createSessionStorage(env: any) {
const cookieSessionStorage = createCookieSessionStorage<
SessionData,
SessionFlashData
>({
cookie: {
name: '__session',
httpOnly: true,
@@ -12,6 +27,5 @@ export function createSessionStorage(env) {
maxAge: 86400 * 7 // 7 days
}
})
return sessionStorage
return cookieSessionStorage
}