update login

This commit is contained in:
2025-11-28 16:07:59 -03:00
parent 2b0efc654a
commit c71e19eacb
14 changed files with 251 additions and 16 deletions

View File

@@ -43,6 +43,7 @@ app.include_router(enrollments.scorm, prefix='/enrollments')
app.include_router(users.router, prefix='/users') app.include_router(users.router, prefix='/users')
app.include_router(users.emails, prefix='/users') app.include_router(users.emails, prefix='/users')
app.include_router(users.orgs, prefix='/users') app.include_router(users.orgs, prefix='/users')
app.include_router(users.password, prefix='/users')
app.include_router(orders.router, prefix='/orders') app.include_router(orders.router, prefix='/orders')
app.include_router(orgs.admins, prefix='/orgs') app.include_router(orgs.admins, prefix='/orgs')
app.include_router(orgs.custom_pricing, prefix='/orgs') app.include_router(orgs.custom_pricing, prefix='/orgs')

View File

@@ -9,8 +9,9 @@ from config import USER_TABLE
from .emails import router as emails from .emails import router as emails
from .orgs import router as orgs from .orgs import router as orgs
from .password import router as password
__all__ = ['emails', 'orgs'] __all__ = ['emails', 'orgs', 'password']
router = Router() router = Router()
dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)

View File

@@ -0,0 +1,42 @@
from http import HTTPStatus
from typing import Annotated
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 layercake.dateutils import now
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
from passlib.hash import pbkdf2_sha256
from api_gateway import JSONResponse
from boto3clients import dynamodb_client
from config import USER_TABLE
router = Router()
dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
class UserNotFoundError(NotFoundError): ...
@router.post('/<user_id>/password')
def password(
user_id: str,
new_password: Annotated[str, Body(min_length=6, embed=True)],
):
with dyn.transact_writer() as transact:
transact.condition(
key=KeyPair(user_id, '0'),
cond_expr='attribute_exists(sk)',
exc_cls=UserNotFoundError,
)
transact.put(
item={
'id': user_id,
'sk': 'PASSWORD',
'hash': pbkdf2_sha256.hash(new_password),
'created_at': now(),
}
)
return JSONResponse(status_code=HTTPStatus.NO_CONTENT)

View File

@@ -0,0 +1,21 @@
from http import HTTPMethod, HTTPStatus
from ...conftest import HttpApiProxy, LambdaContext
def test_password(
app,
seeds,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
):
r = app.lambda_handler(
http_api_proxy(
raw_path='/users/15bacf02-1535-4bee-9022-19d106fd7518/password',
method=HTTPMethod.POST,
body={'new_password': '123@56'},
),
lambda_context,
)
assert r['statusCode'] == HTTPStatus.NO_CONTENT

View File

@@ -47,7 +47,10 @@ export function meta({}: Route.MetaArgs) {
} }
export async function action({ request, context }: Route.ActionArgs) { export async function action({ request, context }: Route.ActionArgs) {
const issuerUrl = new URL('/session', context.cloudflare.env.ISSUER_URL) const issuerUrl = new URL(
'/authentication',
context.cloudflare.env.ISSUER_URL
)
const formData = Object.fromEntries(await request.formData()) const formData = Object.fromEntries(await request.formData())
try { try {
@@ -73,7 +76,7 @@ export async function action({ request, context }: Route.ActionArgs) {
headers.set('Location', url.toString()) headers.set('Location', url.toString())
return new Response(await r.text(), { return new Response(await r.text(), {
status: 302, status: 402,
headers headers
}) })
} catch (error) { } catch (error) {

View File

@@ -9,7 +9,7 @@ export default [
layout('routes/layout.tsx', [ layout('routes/layout.tsx', [
index('routes/index.tsx'), index('routes/index.tsx'),
route('certs', 'routes/certs.tsx'), route('certs', 'routes/certs.tsx'),
route('orders', 'routes/orders.tsx'), route('history', 'routes/history.tsx'),
route('settings', 'routes/settings/layout.tsx', [ route('settings', 'routes/settings/layout.tsx', [
index('routes/settings/profile.tsx'), index('routes/settings/profile.tsx'),
route('emails', 'routes/settings/emails/index.tsx'), route('emails', 'routes/settings/emails/index.tsx'),

View File

@@ -31,7 +31,7 @@ async function proxy({
? await response.text() ? await response.text()
: await response.arrayBuffer() : await response.arrayBuffer()
return new Response(body, { return new Response(body ? body : null, {
status: response.status, status: response.status,
headers: response.headers headers: response.headers
}) })

View File

@@ -42,7 +42,7 @@ const navMain = [
}, },
{ {
title: 'Histórico de compras', title: 'Histórico de compras',
url: '/orders' url: '/history'
} }
] ]

View File

@@ -94,7 +94,7 @@ export function Add() {
<Spinner /> <Spinner />
</div> </div>
)} )}
Adicionar Adicionar email
</Button> </Button>
</div> </div>
</fieldset> </fieldset>

View File

@@ -35,12 +35,19 @@ export function Primary({ items = [] }: { items: Email[] }) {
async ({ email }) => { async ({ email }) => {
// Doesn't use `user` because the data could be outdated // Doesn't use `user` because the data could be outdated
const selected = emails.find((e) => e.email === email) const selected = emails.find((e) => e.email === email)
const new_email = selected?.email
const old_email = primary?.email
if (new_email === old_email) {
return
}
const r = await fetch(`/api/users/${user.id}/emails/primary`, { const r = await fetch(`/api/users/${user.id}/emails/primary`, {
method: 'PATCH', method: 'PATCH',
headers: new Headers({ 'Content-Type': 'application/json' }), headers: new Headers({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ body: JSON.stringify({
new_email: selected?.email, new_email,
old_email: primary?.email, old_email,
email_verified: selected?.email_verified email_verified: selected?.email_verified
}) })
}) })
@@ -87,7 +94,7 @@ export function Primary({ items = [] }: { items: Email[] }) {
})} })}
</NativeSelect> </NativeSelect>
<Button type="submit" className="overflow-hidden cursor-pointer"> <Button type="submit" className="overflow-hidden cursor-pointer">
Alterar Alterar email
</Button> </Button>
</form> </form>
</CardContent> </CardContent>

View File

@@ -1,5 +1,164 @@
import type { Route } from './+types/password' import type { Route } from './+types/password'
export default function Route({}: Route.ComponentProps) { import { useToggle } from 'ahooks'
return <></> import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { toast } from 'sonner'
import { useFetcher } from 'react-router'
import { useEffect } from 'react'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@repo/ui/components/ui/form'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '@repo/ui/components/ui/card'
import { Input } from '@repo/ui/components/ui/input'
import { Button } from '@repo/ui/components/ui/button'
import { Spinner } from '@repo/ui/components/ui/spinner'
import { Checkbox } from '@repo/ui/components/ui/checkbox'
import { Label } from '@repo/ui/components/ui/label'
import { request as req, HttpMethod } from '@repo/util/request'
import { userContext } from '@repo/auth/context'
import type { User } from '@repo/auth/auth'
const formSchema = z
.object({
new_password: z.string().min(6, 'Deve ter no mínimo 6 caracteres'),
confirm_password: z.string().min(6, 'Deve ter no mínimo 6 caracteres')
})
.refine((data) => data.new_password === data.confirm_password, {
message: 'As senhas não coincidem',
path: ['confirm_password']
})
type Schema = z.infer<typeof formSchema>
export async function action({ request, context }: Route.ActionArgs) {
const user = context.get(userContext) as User
const body = await request.json()
const r = await req({
url: `users/${user.sub}/password`,
headers: new Headers({ 'Content-Type': 'application/json' }),
method: HttpMethod.POST,
body: JSON.stringify(body),
request,
context
})
if (!r.ok) {
const error = await r.json().catch(() => ({}))
return { ok: false, error }
}
return { ok: true }
}
export default function Route({}: Route.ComponentProps) {
const [show, { toggle }] = useToggle()
const inputType = show ? 'text' : 'password'
const fetcher = useFetcher()
const form = useForm({
resolver: zodResolver(formSchema)
})
const { control, formState, handleSubmit, reset } = form
const onSubmit = async ({ new_password }: Schema) => {
await fetcher.submit(JSON.stringify({ new_password }), {
method: 'post',
encType: 'application/json'
})
}
useEffect(() => {
if (fetcher.data?.ok) {
toast.success('A senha foi alterada')
return reset()
}
switch (fetcher.data?.error?.type) {
case 'UserConflictError':
toast.error('O colaborador já foi vinculado anteriormente')
}
}, [fetcher.data, reset])
return (
<Form {...form}>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-2.5">
<Card>
<CardHeader>
<CardTitle className="text-lg">Alterar senha</CardTitle>
<CardDescription>
Sua senha deve ter no mínimo 6 caracteres. Recomendamos que inclua
uma combinação de números, letras e caracteres especiais (ex.:
!$@%).
</CardDescription>
</CardHeader>
<CardContent className="space-y-2.5">
<FormField
control={control}
name="new_password"
defaultValue=""
render={({ field }) => (
<FormItem>
<FormLabel>Nova senha</FormLabel>
<FormControl>
<Input type={inputType} autoComplete="false" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="confirm_password"
defaultValue=""
render={({ field }) => (
<FormItem>
<FormLabel>Confirmar nova senha</FormLabel>
<FormControl>
<Input type={inputType} autoComplete="false" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-center gap-3">
<Checkbox id="showPassword" onClick={toggle} tabIndex={-1} />
<Label htmlFor="showPassword" className="cursor-pointer">
Mostrar senha
</Label>
</div>
</CardContent>
</Card>
<div className="flex justify-end">
<Button
type="submit"
className="relative overflow-hidden cursor-pointer"
>
{formState.isSubmitting && (
<div className="absolute inset-0 bg-lime-500 flex items-center justify-center">
<Spinner />
</div>
)}
Alterar senha
</Button>
</div>
</form>
</Form>
)
} }

View File

@@ -55,9 +55,10 @@ export default function Route({}: Route.ComponentProps) {
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-2xl">Minha conta</CardTitle> <CardTitle className="text-2xl">Meu perfil</CardTitle>
<CardDescription> <CardDescription>
Gerenciar as configurações da sua conta. Mantenha os dados do seu perfil atualizados para que apareçam
corretamente em seus cursos e certificados.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
@@ -136,7 +137,7 @@ export default function Route({}: Route.ComponentProps) {
disabled={formState.isSubmitting} disabled={formState.isSubmitting}
> >
{formState.isSubmitting && <Spinner />} {formState.isSubmitting && <Spinner />}
Editar Atualizar perfil
</Button> </Button>
</div> </div>
</form> </form>

View File

@@ -112,7 +112,7 @@ export function NavUser({
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link <Link
to="//scorm.eduseg.workers.dev/orders" to="//scorm.eduseg.workers.dev/history"
className="cursor-pointer" className="cursor-pointer"
> >
<DollarSignIcon /> <DollarSignIcon />