remove subscription endpoint

This commit is contained in:
2026-01-17 16:39:48 -03:00
parent d26abc02c9
commit d0a9998bf4
19 changed files with 450 additions and 192 deletions

View File

@@ -50,6 +50,7 @@ app.include_router(users.password, prefix='/users')
app.include_router(orders.router, prefix='/orders') app.include_router(orders.router, prefix='/orders')
app.include_router(orders.checkout, prefix='/orders') app.include_router(orders.checkout, prefix='/orders')
app.include_router(orders.payment_retries, prefix='/orders') app.include_router(orders.payment_retries, prefix='/orders')
app.include_router(orgs.router, prefix='/orgs')
app.include_router(orgs.add, prefix='/orgs') app.include_router(orgs.add, prefix='/orgs')
app.include_router(orgs.address, prefix='/orgs') app.include_router(orgs.address, prefix='/orgs')
app.include_router(orgs.admins, prefix='/orgs') app.include_router(orgs.admins, prefix='/orgs')

View File

@@ -38,6 +38,9 @@ class UserConflictError(ConflictError): ...
class EmailConflictError(ConflictError): ... class EmailConflictError(ConflictError): ...
class CNPJConflictError(ConflictError): ...
class CPFConflictError(ConflictError): ... class CPFConflictError(ConflictError): ...

View File

@@ -29,7 +29,10 @@ def get_scorm(enrollment_id: str):
if not enrollment: if not enrollment:
raise NotFoundError('Enrollment not found') raise NotFoundError('Enrollment not found')
spec = {'id': 'course.id', 'scormset': 'course.scormset'} spec = {
'id': 'course.id',
'scormset': 'course.scormset',
}
course_id, scormset_id = glom(enrollment, spec).values() course_id, scormset_id = glom(enrollment, spec).values()
scormset = dyn.collection.get_item( scormset = dyn.collection.get_item(
KeyPair(course_id, f'SCORMSET#{scormset_id}'), KeyPair(course_id, f'SCORMSET#{scormset_id}'),

View File

@@ -1,3 +1,10 @@
from aws_lambda_powertools import Logger
from aws_lambda_powertools.event_handler.api_gateway import Router
from layercake.dynamodb import DynamoDBPersistenceLayer, SortKey, TransactKey
from boto3clients import dynamodb_client
from config import USER_TABLE
from .add import router as add from .add import router as add
from .address import router as address from .address import router as address
from .admins import router as admins from .admins import router as admins
@@ -23,3 +30,18 @@ __all__ = [
'users', 'users',
'batch_jobs', 'batch_jobs',
] ]
logger = Logger(__name__)
router = Router()
dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
@router.get('/<org_id>')
def get_org(org_id: str):
return dyn.collection.get_items(
TransactKey(org_id)
+ SortKey('0')
+ SortKey('METADATA#ADDRESS', rename_key='address')
+ SortKey('METADATA#SUBSCRIPTION', rename_key='subscription')
)

View File

@@ -3,7 +3,6 @@ from typing import Annotated
from uuid import uuid4 from uuid import uuid4
from aws_lambda_powertools.event_handler.api_gateway import Router from aws_lambda_powertools.event_handler.api_gateway import Router
from aws_lambda_powertools.event_handler.exceptions import NotFoundError
from aws_lambda_powertools.event_handler.openapi.params import Body from aws_lambda_powertools.event_handler.openapi.params import Body
from layercake.dateutils import now from layercake.dateutils import now
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
@@ -13,24 +12,17 @@ from pydantic import UUID4, BaseModel, EmailStr
from api_gateway import JSONResponse from api_gateway import JSONResponse
from boto3clients import dynamodb_client from boto3clients import dynamodb_client
from config import INTERNAL_EMAIL_DOMAIN, USER_TABLE from config import INTERNAL_EMAIL_DOMAIN, USER_TABLE
from exceptions import ConflictError from exceptions import (
CNPJConflictError,
EmailConflictError,
EmailNotFoundError,
UserNotFoundError,
)
router = Router() router = Router()
dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
class CNPJConflictError(ConflictError): ...
class EmailConflictError(ConflictError): ...
class UserNotFoundError(NotFoundError): ...
class EmailNotFoundError(NotFoundError): ...
class User(BaseModel): class User(BaseModel):
id: str | UUID4 id: str | UUID4
name: NameStr name: NameStr
@@ -101,6 +93,14 @@ def add(
'created_at': now_, 'created_at': now_,
} }
) )
transact.put(
item={
# Post-migration (users): rename `orgmembers#` to `MEMBER#ORG#`
'id': f'orgmembers#{org_id}',
'sk': user.id,
'created_at': now_,
}
)
transact.put( transact.put(
item={ item={
'id': user.id, 'id': user.id,
@@ -119,14 +119,6 @@ def add(
'created_at': now_, 'created_at': now_,
} }
) )
transact.put(
item={
# Post-migration (users): rename `orgmembers#` to `MEMBER#ORG#`
'id': f'orgmembers#{org_id}',
'sk': user.id,
'created_at': now_,
}
)
transact.condition( transact.condition(
key=KeyPair(str(user.id), '0'), key=KeyPair(str(user.id), '0'),
cond_expr='attribute_exists(sk)', cond_expr='attribute_exists(sk)',

View File

@@ -16,15 +16,6 @@ router = Router()
dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
@router.get('/<org_id>/address')
def address(org_id: str):
return dyn.collection.get_item(
KeyPair(org_id, 'METADATA#ADDRESS'),
raise_on_error=False,
default={},
)
class Address(BaseModel): class Address(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True) model_config = ConfigDict(str_strip_whitespace=True)

View File

@@ -22,18 +22,6 @@ class PaymentMethod(str, Enum):
MANUAL = 'MANUAL' MANUAL = 'MANUAL'
@router.get('/<org_id>/subscription')
def subscription(org_id: str):
return dyn.collection.get_item(
KeyPair(
pk=org_id,
sk='METADATA#SUBSCRIPTION',
),
raise_on_error=False,
default={},
)
@router.post('/<org_id>/subscription') @router.post('/<org_id>/subscription')
def add( def add(
org_id: str, org_id: str,
@@ -44,8 +32,13 @@ def add(
now_ = now() now_ = now()
with dyn.transact_writer() as transact: with dyn.transact_writer() as transact:
transact.condition( transact.update(
key=KeyPair(org_id, '0'), key=KeyPair(org_id, '0'),
update_expr='SET subscription_covered = :true, updated_at = :now',
expr_attr_values={
':true': True,
':now': now_,
},
cond_expr='attribute_exists(sk)', cond_expr='attribute_exists(sk)',
exc_cls=OrgNotFoundError, exc_cls=OrgNotFoundError,
) )

View File

@@ -3,7 +3,6 @@ from typing import Annotated
from aws_lambda_powertools.event_handler.api_gateway import Router from aws_lambda_powertools.event_handler.api_gateway import Router
from aws_lambda_powertools.event_handler.exceptions import ( from aws_lambda_powertools.event_handler.exceptions import (
NotFoundError,
ServiceError, ServiceError,
) )
from aws_lambda_powertools.event_handler.openapi.params import Body from aws_lambda_powertools.event_handler.openapi.params import Body
@@ -19,6 +18,7 @@ from layercake.extra_types import CpfStr, NameStr
from api_gateway import JSONResponse from api_gateway import JSONResponse
from boto3clients import dynamodb_client from boto3clients import dynamodb_client
from config import USER_TABLE from config import USER_TABLE
from exceptions import CPFConflictError, UserNotFoundError
from .emails import router as emails from .emails import router as emails
from .orgs import router as orgs from .orgs import router as orgs
@@ -30,14 +30,6 @@ router = Router()
dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
class UserNotFoundError(NotFoundError): ...
class CPFConflictError(ServiceError):
def __init__(self, msg: str | dict):
super().__init__(HTTPStatus.CONFLICT, msg)
class RateLimitExceededError(ServiceError): class RateLimitExceededError(ServiceError):
def __init__(self, msg: str | dict): def __init__(self, msg: str | dict):
super().__init__(HTTPStatus.TOO_MANY_REQUESTS, msg) super().__init__(HTTPStatus.TOO_MANY_REQUESTS, msg)

View File

@@ -75,9 +75,9 @@ export function WorkspaceSwitcher() {
className="aria-expanded:border flex aspect-square size-8 items-center justify-center rounded-lg relative" className="aria-expanded:border flex aspect-square size-8 items-center justify-center rounded-lg relative"
aria-expanded={state === 'expanded'} aria-expanded={state === 'expanded'}
> >
{subscription && ( {subscription ? (
<BadgeCheckIcon className="fill-blue-500 stroke-white absolute size-4 dark:size-3.5 -top-1.5 -right-1.5" /> <BadgeCheckIcon className="fill-blue-500 stroke-white absolute size-4 dark:size-3.5 -top-1.5 -right-1.5" />
)} ) : null}
{initials(activeWorkspace.name)} {initials(activeWorkspace.name)}
</div> </div>

View File

@@ -53,33 +53,18 @@ export const workspaceMiddleware = async (
({ id }) => id === org_id ({ id }) => id === org_id
) as Workspace ) as Workspace
const [subscription, address] = await Promise.all([ const org = (await req({
req({ url: `/orgs/${activeWorkspace.id}`,
url: `/orgs/${activeWorkspace.id}/subscription`,
request, request,
context context
}) }).then((r) => r.json())) as any
.then((r) => r.json())
.then(emptyObjectToNull),
req({
url: `/orgs/${activeWorkspace.id}/address`,
request,
context
})
.then((r) => r.json())
.then(emptyObjectToNull)
])
context.set(workspaceContext, { context.set(workspaceContext, {
activeWorkspace, activeWorkspace,
workspaces, workspaces,
subscription, subscription: org?.['subscription'] || null,
address address: org?.['address'] || null
}) })
return await next() return await next()
} }
const emptyObjectToNull = (data: any) =>
data && Object.keys(data).length === 0 ? null : data

View File

@@ -34,6 +34,7 @@ import { cn } from '@repo/ui/lib/utils'
import { request as req } from '@repo/util/request' import { request as req } from '@repo/util/request'
import { TZ } from '@/conf' import { TZ } from '@/conf'
import { workspaceContext } from '@/middleware/workspace'
import { statuses } from './data' import { statuses } from './data'
import { RangePeriod } from './range-period' import { RangePeriod } from './range-period'
import { billingPeriod, formatDate } from './util' import { billingPeriod, formatDate } from './util'
@@ -43,16 +44,9 @@ export function meta({}) {
} }
export async function loader({ context, request, params }: Route.LoaderArgs) { export async function loader({ context, request, params }: Route.LoaderArgs) {
const workspace = context.get(workspaceContext)
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)
const subscription = await req({ const { billing_day = 1 } = workspace?.subscription || {}
url: `/orgs/${params.orgid}/subscription`,
context,
request
})
const { billing_day = 1 } = (await subscription.json()) as {
billing_day: number
}
const [startDate, endDate] = billingPeriod( const [startDate, endDate] = billingPeriod(
billing_day, billing_day,

View File

@@ -9,6 +9,7 @@ import {
CircleCheckIcon, CircleCheckIcon,
CircleXIcon, CircleXIcon,
EllipsisIcon, EllipsisIcon,
ExternalLinkIcon,
HelpCircleIcon HelpCircleIcon
} from 'lucide-react' } from 'lucide-react'
import { useEffect } from 'react' import { useEffect } from 'react'
@@ -355,9 +356,14 @@ function BankSlipPaymentMethod({
{invoice?.bank_slip ? ( {invoice?.bank_slip ? (
<> <>
<Button variant="link" asChild> <Button
size="sm"
variant="secondary"
className="cursor-pointer"
asChild
>
<a href={invoice.bank_slip.bank_slip_pdf_url} target="_blank"> <a href={invoice.bank_slip.bank_slip_pdf_url} target="_blank">
Abrir o boleto bancário <ExternalLinkIcon /> Abrir o boleto bancário
</a> </a>
</Button> </Button>
</> </>

View File

@@ -1,13 +1,18 @@
import type { Route } from './+types/route' import type { Route } from './+types/route'
import { useEffect } from 'react'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { PatternFormat } from 'react-number-format'
import { useFetcher, Link, useOutletContext } from 'react-router'
import { AlertCircleIcon } from 'lucide-react' import { AlertCircleIcon } from 'lucide-react'
import { useEffect } from 'react'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { PatternFormat } from 'react-number-format'
import { Link, useFetcher, useOutletContext } from 'react-router'
import { toast } from 'sonner' import { toast } from 'sonner'
import {
Alert,
AlertDescription,
AlertTitle
} from '@repo/ui/components/ui/alert'
import { Button } from '@repo/ui/components/ui/button' import { Button } from '@repo/ui/components/ui/button'
import { import {
Card, Card,
@@ -27,13 +32,9 @@ import {
import { Input } from '@repo/ui/components/ui/input' import { Input } from '@repo/ui/components/ui/input'
import { Spinner } from '@repo/ui/components/ui/spinner' import { Spinner } from '@repo/ui/components/ui/spinner'
import { type User } from '@repo/ui/routes/users/data' import { type User } from '@repo/ui/routes/users/data'
import { import { HttpMethod, request as req } from '@repo/util/request'
Alert,
AlertDescription,
AlertTitle
} from '@repo/ui/components/ui/alert'
import { request as req, HttpMethod } from '@repo/util/request'
import { FieldSet } from '@repo/ui/components/ui/field'
import { formSchema, type Schema } from '../_.$orgid.users.add/data' import { formSchema, type Schema } from '../_.$orgid.users.add/data'
export async function action({ params, request, context }: Route.ActionArgs) { export async function action({ params, request, context }: Route.ActionArgs) {
@@ -109,7 +110,8 @@ export default function Route({}: Route.ComponentProps) {
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent>
<FieldSet>
<FormField <FormField
control={control} control={control}
name="name" name="name"
@@ -175,6 +177,7 @@ export default function Route({}: Route.ComponentProps) {
)} )}
/> />
<div className="flex justify-end">
<Button <Button
type="submit" type="submit"
className="cursor-pointer" className="cursor-pointer"
@@ -183,6 +186,8 @@ export default function Route({}: Route.ComponentProps) {
{formState.isSubmitting && <Spinner />} {formState.isSubmitting && <Spinner />}
Editar Editar
</Button> </Button>
</div>
</FieldSet>
</CardContent> </CardContent>
</Card> </Card>
</fieldset> </fieldset>

View File

@@ -1,23 +1,23 @@
import type { Route } from './+types/route' import type { Route } from './+types/route'
import * as cookie from 'cookie' import * as cookie from 'cookie'
import { Outlet, type ShouldRevalidateFunctionArgs } from 'react-router'
import { useEffect } from 'react' import { useEffect } from 'react'
import { Outlet, type ShouldRevalidateFunctionArgs } from 'react-router'
import { userContext } from '@repo/auth/context'
import { authMiddleware } from '@repo/auth/middleware/auth'
import { ModeToggle, ThemedImage } from '@repo/ui/components/dark-mode'
import { NavUser } from '@repo/ui/components/nav-user'
import { import {
SidebarInset, SidebarInset,
SidebarProvider, SidebarProvider,
SidebarTrigger SidebarTrigger
} from '@repo/ui/components/ui/sidebar' } from '@repo/ui/components/ui/sidebar'
import { userContext } from '@repo/auth/context'
import { authMiddleware } from '@repo/auth/middleware/auth'
import { Toaster } from '@repo/ui/components/ui/sonner' import { Toaster } from '@repo/ui/components/ui/sonner'
import { ModeToggle, ThemedImage } from '@repo/ui/components/dark-mode'
import { NavUser } from '@repo/ui/components/nav-user'
import { WorkspaceProvider } from '@/components/workspace-switcher'
import { AppSidebar } from '@/components/app-sidebar' import { AppSidebar } from '@/components/app-sidebar'
import { workspaceMiddleware, workspaceContext } from '@/middleware/workspace' import { WorkspaceProvider } from '@/components/workspace-switcher'
import { workspaceContext, workspaceMiddleware } from '@/middleware/workspace'
// import { Notification } from '@/components/notification' // import { Notification } from '@/components/notification'

View File

@@ -5,18 +5,19 @@ import { useToggle } from 'ahooks'
import { EllipsisIcon, FileBadgeIcon } from 'lucide-react' import { EllipsisIcon, FileBadgeIcon } from 'lucide-react'
import type { ComponentProps } from 'react' import type { ComponentProps } from 'react'
import {
DataTableColumnHeaderSelect,
DataTableColumnSelect
} from '@repo/ui/components/data-table/column-select'
import { Button } from '@repo/ui/components/ui/button' import { Button } from '@repo/ui/components/ui/button'
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger DropdownMenuTrigger
} from '@repo/ui/components/ui/dropdown-menu' } from '@repo/ui/components/ui/dropdown-menu'
import { Spinner } from '@repo/ui/components/ui/spinner' import { Spinner } from '@repo/ui/components/ui/spinner'
import {
DataTableColumnHeaderSelect,
DataTableColumnSelect
} from '@repo/ui/components/data-table/column-select'
import { import {
columns as columns_, columns as columns_,
type Enrollment type Enrollment
@@ -65,12 +66,13 @@ function ActionMenu({ row }: { row: any }) {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-46 *:cursor-pointer"> <DropdownMenuContent align="end" className="w-46 *:cursor-pointer">
<CopyToClipboardItem text={row.id} />
<DownloadItem <DownloadItem
id={row.id} id={row.id}
disabled={!cert} disabled={!cert}
onSuccess={() => setOpen(false)} onSuccess={() => setOpen(false)}
/> />
<DropdownMenuSeparator />
<CopyToClipboardItem text={row.id} />
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>

View File

@@ -0,0 +1,168 @@
import type { Route } from './+types/route'
import { useForm } from 'react-hook-form'
import { Link } from 'react-router'
import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar'
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from '@repo/ui/components/ui/breadcrumb'
import { Button } from '@repo/ui/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '@repo/ui/components/ui/card'
import { FieldSet } from '@repo/ui/components/ui/field'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@repo/ui/components/ui/form'
import { Input } from '@repo/ui/components/ui/input'
import { initials } from '@repo/ui/lib/utils'
import { request as req } from '@repo/util/request'
import { BadgeCheckIcon } from 'lucide-react'
export function meta() {
return [
{
title: 'Editar empresa'
}
]
}
export async function loader({ params, request, context }: Route.LoaderArgs) {
const r = await req({
url: `/orgs/${params.id}`,
request,
context
})
if (!r.ok) {
throw new Response(null, { status: r.status })
}
return { org: await r.json() } as { org: any }
}
export default function Route({ loaderData: { org } }: Route.ComponentProps) {
const form = useForm({ defaultValues: org })
const { handleSubmit } = form
const onSubmit = async () => {}
return (
<div className="space-y-2.5">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to="../orgs">Empresas</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Editar empresa</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="lg:max-w-2xl mx-auto space-y-2.5">
<div className="flex gap-2.5 items-center mb-5">
<div className="relative">
{org?.subscription ? (
<BadgeCheckIcon className="fill-blue-500 stroke-white absolute size-4 dark:size-3.5 -top-0 -right-0 z-2" />
) : null}
<Avatar className="size-12">
<AvatarFallback>{initials(org.name)}</AvatarFallback>
</Avatar>
</div>
<ul>
<li className="font-bold text-lg">{org.name}</li>
<li className="text-muted-foreground text-sm truncate max-lg:max-w-62">
{org.email}
</li>
</ul>
</div>
<Form {...form}>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-8">
<Card>
<CardHeader>
<CardTitle className="font-semibold text-2xl">
Editar empresa
</CardTitle>
<CardDescription>
Configurar as informações gerais para esta empresa.
</CardDescription>
</CardHeader>
<CardContent>
<FieldSet disabled={true}>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Nome</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="cnpj"
render={({ field }) => (
<FormItem>
<FormLabel>CNPJ</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit">Editar</Button>
</div>
</FieldSet>
</CardContent>
</Card>
</form>
</Form>
</div>
</div>
)
}

View File

@@ -1,23 +1,27 @@
'use client' 'use client'
import { type ColumnDef } from '@tanstack/react-table' import { type ColumnDef } from '@tanstack/react-table'
import { EllipsisIcon } from 'lucide-react' import { EllipsisIcon, PencilIcon } from 'lucide-react'
import { NavLink } from 'react-router'
import { Button } from '@repo/ui/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger
} from '@repo/ui/components/ui/dropdown-menu'
import { Abbr } from '@repo/ui/components/abbr' import { Abbr } from '@repo/ui/components/abbr'
import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar'
import { initials } from '@repo/ui/lib/utils'
import { import {
DataTableColumnCpfCnpj, DataTableColumnCpfCnpj,
DataTableColumnDatetime, DataTableColumnDatetime,
DataTableColumnHeaderSelect, DataTableColumnHeaderSelect,
DataTableColumnSelect DataTableColumnSelect
} from '@repo/ui/components/data-table' } from '@repo/ui/components/data-table'
import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar'
import { Button } from '@repo/ui/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@repo/ui/components/ui/dropdown-menu'
import { Spinner } from '@repo/ui/components/ui/spinner'
import { initials } from '@repo/ui/lib/utils'
import type { Org } from '@repo/ui/routes/orgs/data' import type { Org } from '@repo/ui/routes/orgs/data'
import { CopyToClipboardItem } from '../_app.users._index/columns' import { CopyToClipboardItem } from '../_app.users._index/columns'
@@ -86,6 +90,17 @@ function ActionMenu({ row }: { row: any }) {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-36 *:cursor-pointer"> <DropdownMenuContent align="end" className="w-36 *:cursor-pointer">
<DropdownMenuItem asChild onSelect={(e) => e.preventDefault()}>
<NavLink to={`${row.id}`}>
{({ isPending }) => (
<>
{isPending ? <Spinner /> : <PencilIcon />}
Editar
</>
)}
</NavLink>
</DropdownMenuItem>
<DropdownMenuSeparator />
<CopyToClipboardItem text={row.id} /> <CopyToClipboardItem text={row.id} />
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>

View File

@@ -53,7 +53,7 @@ def test_invalid_password(
assert r['statusCode'] == HTTPStatus.UNAUTHORIZED assert r['statusCode'] == HTTPStatus.UNAUTHORIZED
failed = dynamodb_persistence_layer.collection.get_item( failed = dynamodb_persistence_layer.collection.get_item(
KeyPair('357db1c5-7442-4075-98a3-fbe5c938a419', 'FAILED_ATTEMPTS') KeyPair('357db1c5-7442-4075-98a3-fbe5c938a419', 'LOGIN#FAILED_ATTEMPTS')
) )
assert 'failed_attempts' in failed assert 'failed_attempts' in failed

View File

@@ -15,6 +15,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
] ]
[[package]]
name = "anyio"
version = "4.12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
]
[[package]] [[package]]
name = "arnparse" name = "arnparse"
version = "0.0.2" version = "0.0.2"
@@ -266,6 +278,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/1a/aff8bb287a4b1400f69e09a53bd65de96aa5cee5691925b38731c67fc695/click_default_group-1.2.4-py2.py3-none-any.whl", hash = "sha256:9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f", size = 4123, upload-time = "2023-08-04T07:54:56.875Z" }, { url = "https://files.pythonhosted.org/packages/2c/1a/aff8bb287a4b1400f69e09a53bd65de96aa5cee5691925b38731c67fc695/click_default_group-1.2.4-py2.py3-none-any.whl", hash = "sha256:9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f", size = 4123, upload-time = "2023-08-04T07:54:56.875Z" },
] ]
[[package]]
name = "cloudflare"
version = "4.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "distro" },
{ name = "httpx" },
{ name = "pydantic" },
{ name = "sniffio" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5d/48/e481c0a9b9010a5c41b5ca78ff9fbe00dc8a9a4d39da5af610a4ec49c7f7/cloudflare-4.3.1.tar.gz", hash = "sha256:b1e1c6beeb8d98f63bfe0a1cba874fc4e22e000bcc490544f956c689b3b5b258", size = 1933187, upload-time = "2025-06-16T21:43:18.716Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/8f/c6c543565efd3144da4304efa5917aac06b6416a8663a6defe0e9b2b7569/cloudflare-4.3.1-py3-none-any.whl", hash = "sha256:6927135a5ee5633d6e2e1952ca0484745e933727aeeb189996d2ad9d292071c6", size = 4406465, upload-time = "2025-06-16T21:43:17.3Z" },
]
[[package]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.6" version = "0.4.6"
@@ -350,6 +379,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/47/ef/4cb333825d10317a36a1154341ba37e6e9c087bac99c1990ef07ffdb376f/dictdiffer-0.9.0-py2.py3-none-any.whl", hash = "sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595", size = 16754, upload-time = "2021-07-22T13:24:26.783Z" }, { url = "https://files.pythonhosted.org/packages/47/ef/4cb333825d10317a36a1154341ba37e6e9c087bac99c1990ef07ffdb376f/dictdiffer-0.9.0-py2.py3-none-any.whl", hash = "sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595", size = 16754, upload-time = "2021-07-22T13:24:26.783Z" },
] ]
[[package]]
name = "distro"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
]
[[package]] [[package]]
name = "dnspython" name = "dnspython"
version = "2.7.0" version = "2.7.0"
@@ -419,6 +457,43 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/a2/75fd80784ec33da8d39cf885e8811a4fbc045a90db5e336b8e345e66dbb2/glom-24.11.0-py3-none-any.whl", hash = "sha256:991db7fcb4bfa9687010aa519b7b541bbe21111e70e58fdd2d7e34bbaa2c1fbd", size = 102690, upload-time = "2024-11-02T23:17:46.468Z" }, { url = "https://files.pythonhosted.org/packages/9c/a2/75fd80784ec33da8d39cf885e8811a4fbc045a90db5e336b8e345e66dbb2/glom-24.11.0-py3-none-any.whl", hash = "sha256:991db7fcb4bfa9687010aa519b7b541bbe21111e70e58fdd2d7e34bbaa2c1fbd", size = 102690, upload-time = "2024-11-02T23:17:46.468Z" },
] ]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]] [[package]]
name = "id-saladeaula-digital" name = "id-saladeaula-digital"
version = "0.1.0" version = "0.1.0"
@@ -513,12 +588,13 @@ wheels = [
[[package]] [[package]]
name = "layercake" name = "layercake"
version = "0.11.2" version = "0.12.0"
source = { directory = "../layercake" } source = { directory = "../layercake" }
dependencies = [ dependencies = [
{ name = "arnparse" }, { name = "arnparse" },
{ name = "authlib" }, { name = "authlib" },
{ name = "aws-lambda-powertools", extra = ["all"] }, { name = "aws-lambda-powertools", extra = ["all"] },
{ name = "cloudflare" },
{ name = "dictdiffer" }, { name = "dictdiffer" },
{ name = "ftfy" }, { name = "ftfy" },
{ name = "glom" }, { name = "glom" },
@@ -544,6 +620,7 @@ requires-dist = [
{ name = "arnparse", specifier = ">=0.0.2" }, { name = "arnparse", specifier = ">=0.0.2" },
{ name = "authlib", specifier = ">=1.6.5" }, { name = "authlib", specifier = ">=1.6.5" },
{ name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.23.0" }, { name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.23.0" },
{ name = "cloudflare", specifier = ">=4.3.1" },
{ name = "dictdiffer", specifier = ">=0.9.0" }, { name = "dictdiffer", specifier = ">=0.9.0" },
{ name = "ftfy", specifier = ">=6.3.1" }, { name = "ftfy", specifier = ">=6.3.1" },
{ name = "glom", specifier = ">=24.11.0" }, { name = "glom", specifier = ">=24.11.0" },
@@ -1074,6 +1151,15 @@ s3 = [
{ name = "boto3" }, { name = "boto3" },
] ]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]] [[package]]
name = "sqlite-fts4" name = "sqlite-fts4"
version = "1.0.3" version = "1.0.3"