add flash message
This commit is contained in:
@@ -52,8 +52,13 @@ def add(
|
|||||||
)
|
)
|
||||||
|
|
||||||
with dyn.transact_writer() as transact:
|
with dyn.transact_writer() as transact:
|
||||||
transact.condition(
|
transact.update(
|
||||||
key=KeyPair(user_id, '0'),
|
key=KeyPair(user_id, '0'),
|
||||||
|
# Makes the email searchable
|
||||||
|
update_expr='ADD emails :email',
|
||||||
|
expr_attr_values={
|
||||||
|
':email': {email},
|
||||||
|
},
|
||||||
cond_expr='attribute_exists(sk)',
|
cond_expr='attribute_exists(sk)',
|
||||||
exc_cls=UserNotFoundError,
|
exc_cls=UserNotFoundError,
|
||||||
)
|
)
|
||||||
@@ -75,6 +80,7 @@ def add(
|
|||||||
'sk': email,
|
'sk': email,
|
||||||
'created_at': now_,
|
'created_at': now_,
|
||||||
},
|
},
|
||||||
|
# Prevent duplicate emails
|
||||||
cond_expr='attribute_not_exists(sk)',
|
cond_expr='attribute_not_exists(sk)',
|
||||||
exc_cls=EmailConflictError,
|
exc_cls=EmailConflictError,
|
||||||
)
|
)
|
||||||
@@ -172,7 +178,7 @@ def verify(user_id: str, code: str):
|
|||||||
exc_cls=UserNotFoundError,
|
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')
|
@router.patch('/<user_id>/emails/primary')
|
||||||
@@ -213,13 +219,11 @@ def primary(
|
|||||||
)
|
)
|
||||||
transact.update(
|
transact.update(
|
||||||
key=KeyPair(user_id, '0'),
|
key=KeyPair(user_id, '0'),
|
||||||
update_expr='DELETE emails :email_set \
|
update_expr='SET email = :email, \
|
||||||
SET email = :email, \
|
|
||||||
email_verified = :email_verified, \
|
email_verified = :email_verified, \
|
||||||
updated_at = :now',
|
updated_at = :now',
|
||||||
expr_attr_values={
|
expr_attr_values={
|
||||||
':email': new_email,
|
':email': new_email,
|
||||||
':email_set': {new_email},
|
|
||||||
':email_verified': email_verified,
|
':email_verified': email_verified,
|
||||||
':now': now_,
|
':now': now_,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -44,14 +44,16 @@ def test_add_email(
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert r['statusCode'] == HTTPStatus.CREATED
|
assert r['statusCode'] == HTTPStatus.CREATED
|
||||||
email_verification = dynamodb_persistence_layer.collection.query(
|
r = dynamodb_persistence_layer.collection.query(
|
||||||
KeyPair(
|
KeyPair(
|
||||||
'15bacf02-1535-4bee-9022-19d106fd7518',
|
'15bacf02-1535-4bee-9022-19d106fd7518',
|
||||||
'EMAIL_VERIFICATION',
|
'EMAIL_VERIFICATION',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert 'name' in email_verification['items'][0]
|
items = r['items']
|
||||||
assert email_verification['items'][0]['email'] == 'osergiosiqueira+pytest@gmail.com'
|
|
||||||
|
assert len(items) == 2
|
||||||
|
assert any(x.get('email') == 'osergiosiqueira+pytest@gmail.com' for x in items)
|
||||||
|
|
||||||
|
|
||||||
def test_email_as_primary(
|
def test_email_as_primary(
|
||||||
@@ -59,6 +61,7 @@ def test_email_as_primary(
|
|||||||
seeds,
|
seeds,
|
||||||
http_api_proxy: HttpApiProxy,
|
http_api_proxy: HttpApiProxy,
|
||||||
lambda_context: LambdaContext,
|
lambda_context: LambdaContext,
|
||||||
|
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||||
):
|
):
|
||||||
r = app.lambda_handler(
|
r = app.lambda_handler(
|
||||||
http_api_proxy(
|
http_api_proxy(
|
||||||
@@ -75,6 +78,12 @@ def test_email_as_primary(
|
|||||||
|
|
||||||
assert r['statusCode'] == HTTPStatus.NO_CONTENT
|
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(
|
def test_verify_email(
|
||||||
app,
|
app,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Users
|
// Users
|
||||||
{"id": "213a6682-2c59-4404-9189-12eec0a846d4", "sk": "orgs#f6000f79-6e5c-49a0-952f-3bda330ef278", "name": "Banco do Brasil", "cnpj": "00000000000191"}
|
{"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#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": "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"}
|
{"id": "15bacf02-1535-4bee-9022-19d106fd7518", "sk": "EMAIL_VERIFICATION#0d29c753-55f8-42d2-908b-e4976aafc183", "email": "osergiosiqueira@gmail.com", "name": "Sérgio Rafael de Siqueira"}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { Route } from './+types/layout'
|
|||||||
import { useToggle } from 'ahooks'
|
import { useToggle } from 'ahooks'
|
||||||
import { MenuIcon } from 'lucide-react'
|
import { MenuIcon } from 'lucide-react'
|
||||||
import { Link, NavLink, Outlet } from 'react-router'
|
import { Link, NavLink, Outlet } from 'react-router'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
import { Toaster } from '@repo/ui/components/ui/sonner'
|
import { Toaster } from '@repo/ui/components/ui/sonner'
|
||||||
import { userContext } from '@repo/auth/context'
|
import { userContext } from '@repo/auth/context'
|
||||||
@@ -23,12 +24,31 @@ import {
|
|||||||
SheetTrigger
|
SheetTrigger
|
||||||
} from '@repo/ui/components/ui/sheet'
|
} from '@repo/ui/components/ui/sheet'
|
||||||
import type { User } from '@repo/auth/auth'
|
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 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
|
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 = [
|
const navMain = [
|
||||||
@@ -47,10 +67,16 @@ const navMain = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export default function Component({
|
export default function Component({
|
||||||
loaderData: { user }
|
loaderData: { user, flash }
|
||||||
}: Route.ComponentProps) {
|
}: Route.ComponentProps) {
|
||||||
const [isOpen, { toggle }] = useToggle()
|
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 (
|
return (
|
||||||
<div className="relative flex flex-col flex-1 min-w-0 h-full">
|
<div className="relative flex flex-col flex-1 min-w-0 h-full">
|
||||||
<header
|
<header
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ import { request as req } from '@repo/util/request'
|
|||||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||||
import { Button } from '@repo/ui/components/ui/button'
|
import { Button } from '@repo/ui/components/ui/button'
|
||||||
import { useOutletContext } from 'react-router'
|
import { useOutletContext } from 'react-router'
|
||||||
import type { User as AuthUser } from '@repo/auth/auth'
|
|
||||||
import type { User } from '@repo/ui/routes/users/data'
|
import type { User } from '@repo/ui/routes/users/data'
|
||||||
import {
|
import {
|
||||||
Item,
|
Item,
|
||||||
@@ -59,7 +58,8 @@ import { Primary } from './primary'
|
|||||||
const ActionMenuContext = createContext<Email | null>(null)
|
const ActionMenuContext = createContext<Email | null>(null)
|
||||||
|
|
||||||
export async function loader({ request, context }: Route.LoaderArgs) {
|
export async function loader({ request, context }: Route.LoaderArgs) {
|
||||||
const user = context.get(userContext) as AuthUser
|
const user = context.get(userContext)
|
||||||
|
|
||||||
const data = req({
|
const data = req({
|
||||||
url: `/users/${user.sub}/emails`,
|
url: `/users/${user.sub}/emails`,
|
||||||
request,
|
request,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ export const authMiddleware = async (
|
|||||||
const strategy = authenticator.get<OAuth2Strategy<User>>('oidc')
|
const strategy = authenticator.get<OAuth2Strategy<User>>('oidc')
|
||||||
const session = await sessionStorage.getSession(request.headers.get('cookie'))
|
const session = await sessionStorage.getSession(request.headers.get('cookie'))
|
||||||
const requestId = context.get(requestIdContext)
|
const requestId = context.get(requestIdContext)
|
||||||
let user = session.get('user') as User | null
|
let user = session.get('user')
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
console.log('There is no user logged in')
|
console.log('There is no user logged in')
|
||||||
@@ -67,7 +67,11 @@ export const authMiddleware = async (
|
|||||||
context.set(userContext, user)
|
context.set(userContext, user)
|
||||||
|
|
||||||
const response = await next()
|
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
|
return response
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,22 @@
|
|||||||
import { createCookieSessionStorage } from 'react-router'
|
import { createCookieSessionStorage } from 'react-router'
|
||||||
|
import { type User } from './auth'
|
||||||
|
|
||||||
export function createSessionStorage(env) {
|
type SessionData = {
|
||||||
const sessionStorage = createCookieSessionStorage({
|
user: User
|
||||||
|
returnTo: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionFlashData = {
|
||||||
|
error: string
|
||||||
|
success: string
|
||||||
|
info: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSessionStorage(env: any) {
|
||||||
|
const cookieSessionStorage = createCookieSessionStorage<
|
||||||
|
SessionData,
|
||||||
|
SessionFlashData
|
||||||
|
>({
|
||||||
cookie: {
|
cookie: {
|
||||||
name: '__session',
|
name: '__session',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
@@ -12,6 +27,5 @@ export function createSessionStorage(env) {
|
|||||||
maxAge: 86400 * 7 // 7 days
|
maxAge: 86400 * 7 // 7 days
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
return cookieSessionStorage
|
||||||
return sessionStorage
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user