diff --git a/api.saladeaula.digital/template.yaml b/api.saladeaula.digital/template.yaml index 1ec099d..5e1ae16 100644 --- a/api.saladeaula.digital/template.yaml +++ b/api.saladeaula.digital/template.yaml @@ -64,14 +64,14 @@ Resources: DefaultAuthorizer: OAuth2Authorizer Authorizers: OAuth2Authorizer: - IdentitySource: '$request.header.Authorization' + IdentitySource: $request.header.Authorization JwtConfiguration: - issuer: 'https://id.saladeaula.digital' + issuer: https://id.saladeaula.digital audience: - - '6fd6a7ec-c956-4f0b-96d7-337ffec6eabb' - - '1a5483ab-4521-4702-9115-5857ac676851' - - '1db63660-063d-4280-b2ea-388aca4a9459' - - '78a0819e-1f9b-4da1-b05f-40ec0eaed0c8' + - 1a5483ab-4521-4702-9115-5857ac676851 # saladeaula.digital + - 6fd6a7ec-c956-4f0b-96d7-337ffec6eabb # insights.saladeaula.digital + - 1db63660-063d-4280-b2ea-388aca4a9459 # admin.saladeaula.digital + - 78a0819e-1f9b-4da1-b05f-40ec0eaed0c8 # studio.saladeaula.digital HttpApiFunction: Type: AWS::Serverless::Function @@ -122,7 +122,7 @@ Resources: ScheduleEvent: Type: ScheduleV2 Properties: - ScheduleExpression: 'cron(*/5 5-23 * * ? *)' + ScheduleExpression: cron(*/5 5-23 * * ? *) ScheduleExpressionTimezone: America/Sao_Paulo Outputs: diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.admins._index/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.admins._index/route.tsx index 8c60147..14ea3bd 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.admins._index/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.admins._index/route.tsx @@ -1,11 +1,7 @@ import type { Route } from './+types' import { useToggle } from 'ahooks' -import { - EllipsisVerticalIcon, - PencilIcon, - UserRoundMinusIcon -} from 'lucide-react' +import { EllipsisIcon, PencilIcon, UserRoundMinusIcon } from 'lucide-react' import { Suspense } from 'react' import { Await, NavLink, useParams, useRevalidator } from 'react-router' import { toast } from 'sonner' @@ -123,7 +119,7 @@ function ActionMenu({ id }: { id: string }) { className="data-[state=open]:bg-muted text-muted-foreground cursor-pointer absolute z-1 right-4 top-4" size="icon-sm" > - + Abrir menu @@ -185,12 +181,12 @@ function RevokeItem({ id }: { id: string }) { - Cancelar + Cancelar diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/columns.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/columns.tsx index e9fc3c2..f6686dd 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/columns.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/columns.tsx @@ -5,7 +5,7 @@ import type { ComponentProps, MouseEvent } from 'react' import { useRequest, useToggle } from 'ahooks' import { CircleXIcon, - EllipsisVerticalIcon, + EllipsisIcon, FileBadgeIcon, LockOpenIcon } from 'lucide-react' @@ -99,7 +99,7 @@ function ActionMenu({ row }: { row: any }) { className="data-[state=open]:bg-muted text-muted-foreground cursor-pointer" size="icon-sm" > - + Abrir menu @@ -238,12 +238,12 @@ function RemoveDedupItem({ )} - Cancelar + Cancelar @@ -305,12 +305,12 @@ function CancelItem({ - Cancelar + Cancelar diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.orders._index/columns.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.orders._index/columns.tsx index da8a6ed..7998337 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.orders._index/columns.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.orders._index/columns.tsx @@ -7,7 +7,6 @@ import { DataTableColumnSelect } from '@repo/ui/components/data-table' import { columns as columns_, type Order } from '@repo/ui/routes/orders/columns' -import { Abbr } from '@repo/ui/components/abbr' export type { Order } diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.$id._index/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.$id._index/route.tsx index 2e07402..20755c2 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.$id._index/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.$id._index/route.tsx @@ -3,6 +3,7 @@ import type { Route } from './+types/route' import { zodResolver } from '@hookform/resolvers/zod' import { PatternFormat } from 'react-number-format' import { Link, useOutletContext } from 'react-router' +import { useForm } from 'react-hook-form' import { Button } from '@repo/ui/components/ui/button' import { @@ -22,15 +23,14 @@ import { } from '@repo/ui/components/ui/form' import { Input } from '@repo/ui/components/ui/input' import { Spinner } from '@repo/ui/components/ui/spinner' -import { useForm } from 'react-hook-form' +import { type User } from '@repo/ui/routes/users/data' -import type { User } from '../_.$orgid.users.$id/route' -import { formSchema, type Schema } from '../_.$orgid.users.add/route' +import { formSchema, type Schema } from '../_.$orgid.users.add/data' export default function Route({}: Route.ComponentProps) { const { user } = useOutletContext() as { user: User } const form = useForm({ - defaultValues: user, + defaultValues: { ...user, given_email: false }, resolver: zodResolver(formSchema) }) const { handleSubmit, control, formState } = form diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.$id/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.$id/route.tsx index 1e264a5..fab0996 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.$id/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.$id/route.tsx @@ -40,8 +40,8 @@ export async function loader({ params, request, context }: Route.LoaderArgs) { throw new Response(null, { status: r.status }) } - const user: User = await r.json() - return { user } + const data = await r.json() + return { data } as { data: any } } export function shouldRevalidate({ @@ -51,18 +51,14 @@ export function shouldRevalidate({ return currentParams.id !== nextParams.id } -export type User = { - name: string - email: string - cpf: string -} - const links = [ { to: '', title: 'Perfil', end: true }, { to: 'emails', title: 'Emails' } ] -export default function Route({ loaderData: { user } }: Route.ComponentProps) { +export default function Route({ + loaderData: { data: user } +}: Route.ComponentProps) { return (
diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users._index/columns.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users._index/columns.tsx index ac016d4..19802c5 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users._index/columns.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users._index/columns.tsx @@ -2,11 +2,7 @@ import { type ColumnDef } from '@tanstack/react-table' import { useToggle } from 'ahooks' -import { - EllipsisVerticalIcon, - PencilIcon, - UserRoundMinusIcon -} from 'lucide-react' +import { EllipsisIcon, PencilIcon, UserRoundMinusIcon } from 'lucide-react' import { NavLink, useParams } from 'react-router' import { toast } from 'sonner' @@ -63,7 +59,7 @@ function ActionMenu({ row }: { row: any }) { className="data-[state=open]:bg-muted text-muted-foreground cursor-pointer" size="icon-sm" > - + Abrir menu @@ -127,12 +123,12 @@ function UnlinkItem({ id }: { id: string }) { - Cancelar + Cancelar diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/data.ts b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/data.ts new file mode 100644 index 0000000..ebc17ee --- /dev/null +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/data.ts @@ -0,0 +1,55 @@ +import { isValidCPF } from '@brazilian-utils/brazilian-utils' +import { + adjectives, + colors, + NumberDictionary, + uniqueNamesGenerator +} from 'unique-names-generator' +import { z } from 'zod' + +const isName = (name: string) => name && name.includes(' ') + +function randomEmail() { + const numberDict = NumberDictionary.generate({ min: 100, max: 999 }) + const randomName: string = uniqueNamesGenerator({ + dictionaries: [adjectives, colors, numberDict], + length: 3, + separator: '-' + }) + + return `${randomName}@users.noreply.saladeaula.digital` +} + +export const formSchema = z + .object({ + name: z + .string() + .trim() + .nonempty('Digite um nome') + .refine(isName, { message: 'Nome inválido' }), + email: z.string().trim().toLowerCase().optional(), + cpf: z + .string('CPF obrigatório') + .refine(isValidCPF, { message: 'CPF inválido' }), + given_email: z.coerce.boolean() + }) + .refine( + ({ given_email, email }) => { + if (given_email) { + return true + } + return email && z.email().safeParse(email).success + }, + { + message: 'Email inválido', + path: ['email'] + } + ) + .transform((data) => { + if (data.given_email) { + return { ...data, email: randomEmail() } + } + return data + }) + +export type Schema = z.infer diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/route.tsx index c7450b9..1301cb7 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/route.tsx @@ -1,18 +1,11 @@ import type { Route } from './+types/route' -import { isValidCPF } from '@brazilian-utils/brazilian-utils' +import { useEffect } from 'react' import { zodResolver } from '@hookform/resolvers/zod' import { useForm } from 'react-hook-form' import { PatternFormat } from 'react-number-format' import { Link, useFetcher } from 'react-router' import { toast } from 'sonner' -import { - adjectives, - colors, - NumberDictionary, - uniqueNamesGenerator -} from 'unique-names-generator' -import { z } from 'zod' import { Breadcrumb, @@ -44,54 +37,7 @@ import { Spinner } from '@repo/ui/components/ui/spinner' import { useWorksapce } from '@/components/workspace-switcher' import { HttpMethod, request as req } from '@repo/util/request' -import { useEffect } from 'react' - -const isName = (name: string) => name && name.includes(' ') - -function randomEmail() { - const numberDict = NumberDictionary.generate({ min: 100, max: 999 }) - const randomName: string = uniqueNamesGenerator({ - dictionaries: [adjectives, colors, numberDict], - length: 3, - separator: '-' - }) - - return `${randomName}@users.noreply.saladeaula.digital` -} - -export const formSchema = z - .object({ - name: z - .string() - .trim() - .nonempty('Digite um nome') - .refine(isName, { message: 'Nome inválido' }), - email: z.string().trim().toLowerCase().optional(), - cpf: z - .string('CPF obrigatório') - .refine(isValidCPF, { message: 'CPF inválido' }), - given_email: z.coerce.boolean() - }) - .refine( - ({ given_email, email }) => { - if (given_email) { - return true - } - return email && z.email().safeParse(email).success - }, - { - message: 'Email inválido', - path: ['email'] - } - ) - .transform((data) => { - if (data.given_email) { - return { ...data, email: randomEmail() } - } - return data - }) - -export type Schema = z.infer +import { formSchema, type Schema } from './data' export function meta({}: Route.MetaArgs) { return [{ title: 'Adicionar colaborador' }] diff --git a/apps/id.saladeaula.digital/app/routes/authorize.ts b/apps/id.saladeaula.digital/app/routes/authorize.ts index 281ed5c..d4b98da 100644 --- a/apps/id.saladeaula.digital/app/routes/authorize.ts +++ b/apps/id.saladeaula.digital/app/routes/authorize.ts @@ -2,10 +2,6 @@ import type { Route } from './+types' import { parse } from 'cookie' -export const OK = 200 -export const FOUND = 302 -export const INTERNAL_SERVER_ERROR = 500 - export async function loader({ request, context }: Route.LoaderArgs) { const cookies = parse(request.headers.get('Cookie') || '') const url = new URL(request.url) @@ -16,7 +12,7 @@ export async function loader({ request, context }: Route.LoaderArgs) { if (!cookies?.__session) { return new Response(null, { - status: FOUND, + status: 302, headers: { Location: loginUrl.toString() } @@ -33,7 +29,7 @@ export async function loader({ request, context }: Route.LoaderArgs) { redirect: 'manual' }) - if (r.status === FOUND) { + if (r.status === 302) { return new Response(await r.text(), { status: r.status, headers: r.headers @@ -47,7 +43,7 @@ export async function loader({ request, context }: Route.LoaderArgs) { }) // Deny authorization if user lacks scopes requested by client - if (r.status === FOUND) { + if (r.status === 302) { return new Response(null, { status: r.status, headers: { @@ -57,13 +53,13 @@ export async function loader({ request, context }: Route.LoaderArgs) { } return new Response(null, { - status: FOUND, + status: 302, headers: { Location: loginUrl.toString() } }) } catch (error) { console.error(error) - return new Response(null, { status: INTERNAL_SERVER_ERROR }) + return new Response(null, { status: 500 }) } } diff --git a/apps/id.saladeaula.digital/app/routes/index.tsx b/apps/id.saladeaula.digital/app/routes/index.tsx index 7b89c2e..b59b088 100644 --- a/apps/id.saladeaula.digital/app/routes/index.tsx +++ b/apps/id.saladeaula.digital/app/routes/index.tsx @@ -79,7 +79,7 @@ export async function action({ request, context }: Route.ActionArgs) { }) } catch (error) { console.error(error) - return Response.json({}, { status: INTERNAL_SERVER_ERROR }) + return Response.json({}, { status: 500 }) } } diff --git a/apps/id.saladeaula.digital/app/routes/layout.tsx b/apps/id.saladeaula.digital/app/routes/layout.tsx index 0ba9494..8cd78af 100644 --- a/apps/id.saladeaula.digital/app/routes/layout.tsx +++ b/apps/id.saladeaula.digital/app/routes/layout.tsx @@ -20,8 +20,8 @@ export default function Layout() { aria-hidden="true" className="absolute inset-0 grid grid-cols-2 opacity-20" > -
-
+
+
diff --git a/apps/insights.saladeaula.digital/app/routes/_app.enrollments._index/columns.tsx b/apps/insights.saladeaula.digital/app/routes/_app.enrollments._index/columns.tsx index 82ac55e..53a1bb5 100644 --- a/apps/insights.saladeaula.digital/app/routes/_app.enrollments._index/columns.tsx +++ b/apps/insights.saladeaula.digital/app/routes/_app.enrollments._index/columns.tsx @@ -2,7 +2,7 @@ import type { ColumnDef } from '@tanstack/react-table' import { useToggle } from 'ahooks' -import { EllipsisVerticalIcon, FileBadgeIcon } from 'lucide-react' +import { EllipsisIcon, FileBadgeIcon } from 'lucide-react' import type { ComponentProps } from 'react' import { Button } from '@repo/ui/components/ui/button' @@ -57,7 +57,7 @@ function ActionMenu({ row }: { row: any }) { className="data-[state=open]:bg-muted text-muted-foreground cursor-pointer" size="icon-sm" > - + Abrir menu diff --git a/apps/insights.saladeaula.digital/app/routes/_app.orgs._index/columns.tsx b/apps/insights.saladeaula.digital/app/routes/_app.orgs._index/columns.tsx index 4731e37..1203a63 100644 --- a/apps/insights.saladeaula.digital/app/routes/_app.orgs._index/columns.tsx +++ b/apps/insights.saladeaula.digital/app/routes/_app.orgs._index/columns.tsx @@ -1,7 +1,7 @@ 'use client' import { type ColumnDef } from '@tanstack/react-table' -import { EllipsisVerticalIcon } from 'lucide-react' +import { EllipsisIcon } from 'lucide-react' import { Button } from '@repo/ui/components/ui/button' import { @@ -81,7 +81,7 @@ function ActionMenu({ row }: { row: any }) { className="data-[state=open]:bg-muted text-muted-foreground cursor-pointer" size="icon-sm" > - + Abrir menu diff --git a/apps/insights.saladeaula.digital/app/routes/_app.payments._index/columns.tsx b/apps/insights.saladeaula.digital/app/routes/_app.payments._index/columns.tsx index 93bf5b4..11df219 100644 --- a/apps/insights.saladeaula.digital/app/routes/_app.payments._index/columns.tsx +++ b/apps/insights.saladeaula.digital/app/routes/_app.payments._index/columns.tsx @@ -1,7 +1,7 @@ 'use client' import { type ColumnDef } from '@tanstack/react-table' -import { EllipsisVerticalIcon } from 'lucide-react' +import { EllipsisIcon } from 'lucide-react' import { Abbr } from '@repo/ui/components/abbr' import { Button } from '@repo/ui/components/ui/button' @@ -78,7 +78,7 @@ function ActionMenu({ row }: { row: any }) { className="data-[state=open]:bg-muted text-muted-foreground cursor-pointer" size="icon-sm" > - + Abrir menu diff --git a/apps/insights.saladeaula.digital/app/routes/_app.users._index/columns.tsx b/apps/insights.saladeaula.digital/app/routes/_app.users._index/columns.tsx index f10eb1d..dbfba3f 100644 --- a/apps/insights.saladeaula.digital/app/routes/_app.users._index/columns.tsx +++ b/apps/insights.saladeaula.digital/app/routes/_app.users._index/columns.tsx @@ -2,7 +2,7 @@ import { useBoolean } from 'ahooks' import { type ColumnDef } from '@tanstack/react-table' -import { CheckIcon, CopyIcon, EllipsisVerticalIcon } from 'lucide-react' +import { CheckIcon, CopyIcon, EllipsisIcon } from 'lucide-react' import { DataTableColumnSelect, @@ -46,7 +46,7 @@ function ActionMenu({ row }: { row: any }) { className="data-[state=open]:bg-muted text-muted-foreground cursor-pointer" size="icon-sm" > - + Abrir menu diff --git a/apps/saladeaula.digital/app/routes.ts b/apps/saladeaula.digital/app/routes.ts index 3795576..86a97b4 100644 --- a/apps/saladeaula.digital/app/routes.ts +++ b/apps/saladeaula.digital/app/routes.ts @@ -10,7 +10,11 @@ export default [ index('routes/index.tsx'), route('certs', 'routes/certs.tsx'), route('orders', 'routes/orders.tsx'), - route('settings', 'routes/settings.tsx'), + route('settings', 'routes/settings/layout.tsx', [ + index('routes/settings/profile.tsx'), + route('emails', 'routes/settings/emails.tsx'), + route('password', 'routes/settings/password.tsx') + ]), route('konviva', 'routes/konviva.ts'), route('player/:id', 'routes/player.tsx'), route('proxy/*', 'routes/proxy.tsx') diff --git a/apps/saladeaula.digital/app/routes/settings.tsx b/apps/saladeaula.digital/app/routes/settings.tsx deleted file mode 100644 index 6f7f154..0000000 --- a/apps/saladeaula.digital/app/routes/settings.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import type { Route } from './+types' - -import { useForm } from 'react-hook-form' -import { Link } from 'react-router' - -import { Container } from '@/components/container' - -import type { User } from '@repo/auth/auth' -import { userContext } from '@repo/auth/context' -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 { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage -} from '@repo/ui/components/ui/form' -import { Input } from '@repo/ui/components/ui/input' -import { Spinner } from '@repo/ui/components/ui/spinner' -import { request as req } from '@repo/util/request' - -export function meta({}: Route.MetaArgs) { - return [{ title: 'Minha conta' }] -} - -export async function loader({ context, request }: Route.ActionArgs) { - const user = context.get(userContext) as User - const r = await req({ - url: `/users/${user.sub}`, - request, - context - }) - return { user: await r.json() } -} - -export default function Route({ loaderData: { user } }) { - const form = useForm({ defaultValues: user }) - const { handleSubmit, control, formState } = form - - const onSubmit = async (data) => { - console.log(data) - } - - return ( - - - - - - Meus cursos - - - - - Minha conta - - - - -
-
- - - - Minha conta - - Gerenciar as configurações da sua conta. - - - - - ( - - Nome - - - - - - )} - /> - - ( - - Email - - - - - - Para gerenciar os emails ou trocar o email principal, - use as{' '} - - configurações de emails - - - - - - )} - /> - - ( - - CPF - - - - - - )} - /> - - - -
- -
-
- -
-
- ) -} diff --git a/apps/saladeaula.digital/app/routes/settings/data.ts b/apps/saladeaula.digital/app/routes/settings/data.ts new file mode 100644 index 0000000..3451dcd --- /dev/null +++ b/apps/saladeaula.digital/app/routes/settings/data.ts @@ -0,0 +1,18 @@ +import { z } from 'zod' +import { isValidCPF } from '@brazilian-utils/brazilian-utils' + +const isName = (name: string) => name && name.includes(' ') + +export const formSchema = z.object({ + name: z + .string() + .trim() + .nonempty('Digite um nome') + .refine(isName, { message: 'Nome inválido' }), + email: z.string().trim().toLowerCase(), + cpf: z + .string('CPF obrigatório') + .refine(isValidCPF, { message: 'CPF inválido' }) +}) + +export type Schema = z.infer diff --git a/apps/saladeaula.digital/app/routes/settings/emails.tsx b/apps/saladeaula.digital/app/routes/settings/emails.tsx new file mode 100644 index 0000000..27ff16f --- /dev/null +++ b/apps/saladeaula.digital/app/routes/settings/emails.tsx @@ -0,0 +1,56 @@ +import type { Route } from './+types/emails' + +import { Suspense } from 'react' +import { Await } from 'react-router' + +import { userContext } from '@repo/auth/context' +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle +} from '@repo/ui/components/ui/card' +import type { User } from '@repo/auth/auth' +import { request as req } from '@repo/util/request' +import { Skeleton } from '@repo/ui/components/skeleton' + +export async function loader({ request, context }: Route.LoaderArgs) { + const user = context.get(userContext) as User + const data = req({ + url: `/users/${user.sub}/emails`, + request, + context + }).then((r) => r.json()) + + return { data } +} + +export default function Route({ loaderData: { data } }: Route.ComponentProps) { + return ( + }> + + {({ items = [] }) => ( + + + Emails + + Podem ser associados vários emails a sua conta. É possível usar + qualquer email para recuperar a senha, mas apenas o email + principal receberá as mensagens. + + + +
    + {items.map(({ sk }: { sk: string }, idx: number) => { + const [, email] = sk.split('#') + return
  • {email}
  • + })} +
+
+
+ )} +
+
+ ) +} diff --git a/apps/saladeaula.digital/app/routes/settings/layout.tsx b/apps/saladeaula.digital/app/routes/settings/layout.tsx new file mode 100644 index 0000000..71fb6e2 --- /dev/null +++ b/apps/saladeaula.digital/app/routes/settings/layout.tsx @@ -0,0 +1,100 @@ +import type { Route } from './+types/layout' + +import { + NavLink, + Outlet, + Link, + type ShouldRevalidateFunctionArgs +} from 'react-router' + +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator +} from '@repo/ui/components/ui/breadcrumb' +import { Tabs, TabsList, TabsTrigger } from '@repo/ui/components/ui/tabs' +import { request as req } from '@repo/util/request' + +import { Container } from '@/components/container' +import { userContext } from '@repo/auth/context' +import type { User } from '@repo/auth/auth' + +export function meta({}: Route.MetaArgs) { + return [{ title: 'Minha conta' }] +} + +export async function loader({ request, context }: Route.LoaderArgs) { + const user = context.get(userContext) as User + const r = await req({ + url: `/users/${user.sub}`, + request, + context + }) + + if (!r.ok) { + throw new Response(null, { status: r.status }) + } + + const data = await r.json() + return { data } +} + +export function shouldRevalidate({}: ShouldRevalidateFunctionArgs) { + return false +} + +const links = [ + { to: '', title: 'Perfil', end: true }, + { to: 'emails', title: 'Emails' }, + { to: 'password', title: 'Senha' } +] + +export default function Layout({ loaderData: { data } }: Route.ComponentProps) { + return ( + + + + + + Meus cursos + + + + + Minha conta + + + + +
+ + + {links.map(({ to, title, ...props }, idx) => ( + + {({ isActive }) => ( + + {title} + + )} + + ))} + + + + +
+
+ ) +} diff --git a/apps/saladeaula.digital/app/routes/settings/password.tsx b/apps/saladeaula.digital/app/routes/settings/password.tsx new file mode 100644 index 0000000..00c96a5 --- /dev/null +++ b/apps/saladeaula.digital/app/routes/settings/password.tsx @@ -0,0 +1,5 @@ +import type { Route } from './+types/emails' + +export default function Route({}: Route.ComponentProps) { + return <> +} diff --git a/apps/saladeaula.digital/app/routes/settings/profile.tsx b/apps/saladeaula.digital/app/routes/settings/profile.tsx new file mode 100644 index 0000000..90fa23d --- /dev/null +++ b/apps/saladeaula.digital/app/routes/settings/profile.tsx @@ -0,0 +1,145 @@ +import type { Route } from './+types/profile' + +import { useForm } from 'react-hook-form' +import { Link, useOutletContext } from 'react-router' +import { PatternFormat } from 'react-number-format' +import { zodResolver } from '@hookform/resolvers/zod' + +import { Button } from '@repo/ui/components/ui/button' +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle +} from '@repo/ui/components/ui/card' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@repo/ui/components/ui/form' +import { Input } from '@repo/ui/components/ui/input' +import { Spinner } from '@repo/ui/components/ui/spinner' +import { type User } from '@repo/ui/routes/users/data' + +import { formSchema, type Schema } from './data' +import { useFetcher } from 'react-router' + +export async function action({ request }: Route.ActionArgs) { + const body = await request.json() + console.log(body) + return { ok: true } +} + +export default function Route({}: Route.ComponentProps) { + const { user } = useOutletContext() as { user: User } + const fetcher = useFetcher() + const form = useForm({ + defaultValues: user, + resolver: zodResolver(formSchema) + }) + const { handleSubmit, control, formState } = form + + const onSubmit = async (data: Schema) => { + await fetcher.submit(JSON.stringify({ id: user.id, ...data }), { + method: 'post', + encType: 'application/json' + }) + } + + return ( +
+ + + + Minha conta + + Gerenciar as configurações da sua conta. + + + + + ( + + Nome + + + + + + )} + /> + + ( + + Email + + + + + + Para gerenciar os emails ou trocar o email principal, use + as{' '} + + configurações de emails + + + + + + )} + /> + + ( + + CPF + + { + onChange(value) + }} + {...props} + /> + + + + )} + /> + + + +
+ +
+
+ + ) +} diff --git a/apps/studio.saladeaula.digital/app/routes/edit.tsx b/apps/studio.saladeaula.digital/app/routes/edit.tsx index 5c33bac..65f75c3 100644 --- a/apps/studio.saladeaula.digital/app/routes/edit.tsx +++ b/apps/studio.saladeaula.digital/app/routes/edit.tsx @@ -253,8 +253,8 @@ function Editing() {
Habilitar certificação @@ -383,7 +383,7 @@ function Editing() { {...field} /> - Ocultar o curso no catálogo. + Ocultar o curso no catálogo )} /> diff --git a/apps/studio.saladeaula.digital/app/routes/index.tsx b/apps/studio.saladeaula.digital/app/routes/index.tsx index 0f572ca..ae2df9e 100644 --- a/apps/studio.saladeaula.digital/app/routes/index.tsx +++ b/apps/studio.saladeaula.digital/app/routes/index.tsx @@ -1,7 +1,13 @@ import type { Route } from './+types/index' import Fuse from 'fuse.js' -import { AwardIcon, BanIcon, FileBadgeIcon, LaptopIcon } from 'lucide-react' +import { + AwardIcon, + BanIcon, + FileBadgeIcon, + HatGlassesIcon, + LaptopIcon +} from 'lucide-react' import { Suspense, useMemo } from 'react' import { Await, NavLink, useSearchParams } from 'react-router' @@ -31,6 +37,7 @@ import { import type { Course } from './edit' import placeholder from '@/assets/placeholder.webp' +import { cn } from '@repo/ui/lib/utils' export function meta({}: Route.MetaArgs) { return [{ title: 'Gerenciar seus cursos' }] @@ -131,7 +138,12 @@ function Course({ id, name, access_period, cert, draft }: Course) { return ( {({ isPending }) => ( - + {isPending && (
@@ -139,12 +151,7 @@ function Course({ id, name, access_period, cert, draft }: Course) { )} - - {name}{' '} - {draft ? ( - (rascunho) - ) : null} - + {name} @@ -180,6 +187,12 @@ function Course({ id, name, access_period, cert, draft }: Course) { )} + + {draft && ( +
  • + +
  • + )}
    diff --git a/id.saladeaula.digital/app/app.py b/id.saladeaula.digital/app/app.py index 6da7259..c23eb3d 100644 --- a/id.saladeaula.digital/app/app.py +++ b/id.saladeaula.digital/app/app.py @@ -7,24 +7,26 @@ from aws_lambda_powertools.event_handler.api_gateway import ( from aws_lambda_powertools.logging import correlation_paths from aws_lambda_powertools.utilities.typing import LambdaContext +from routes.authentication import router as authentication from routes.authorize import router as authorize from routes.jwks import router as jwks from routes.openid_configuration import router as openid_configuration +from routes.register import router as register from routes.revoke import router as revoke -from routes.session import router as session from routes.token import router as token from routes.userinfo import router as userinfo logger = Logger(__name__) tracer = Tracer() app = APIGatewayHttpResolver(enable_validation=True) -app.include_router(session) +app.include_router(authentication) app.include_router(authorize) app.include_router(jwks) +app.include_router(openid_configuration) +app.include_router(register) +app.include_router(revoke) app.include_router(token) app.include_router(userinfo) -app.include_router(revoke) -app.include_router(openid_configuration) @app.get('/health') diff --git a/id.saladeaula.digital/app/oauth2.py b/id.saladeaula.digital/app/oauth2.py index aaa6604..25e97b8 100644 --- a/id.saladeaula.digital/app/oauth2.py +++ b/id.saladeaula.digital/app/oauth2.py @@ -198,23 +198,6 @@ class AuthorizationCodeGrant(grants.AuthorizationCodeGrant): ) -class TokenExchangeGrant(grants.BaseGrant): - GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:token-exchange' - - TOKEN_ENDPOINT_AUTH_METHODS = [ - 'client_secret_basic', - 'client_secret_post', - ] - - @hooked - def validate_token_request(self): - raise NotImplementedError() - - @hooked - def create_token_response(self): - raise NotImplementedError() - - class RefreshTokenGrant(grants.RefreshTokenGrant): TOKEN_ENDPOINT_AUTH_METHODS = [ 'client_secret_basic', @@ -280,6 +263,23 @@ class RefreshTokenGrant(grants.RefreshTokenGrant): ) +class TokenExchangeGrant(grants.BaseGrant): + GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:token-exchange' + + TOKEN_ENDPOINT_AUTH_METHODS = [ + 'client_secret_basic', + 'client_secret_post', + ] + + @hooked + def validate_token_request(self): + raise NotImplementedError() + + @hooked + def create_token_response(self): + raise NotImplementedError() + + class RevocationEndpoint(rfc7009.RevocationEndpoint): def query_token( # type: ignore self, diff --git a/id.saladeaula.digital/app/routes/authorize.py b/id.saladeaula.digital/app/routes/authorize.py index c913109..7df328c 100644 --- a/id.saladeaula.digital/app/routes/authorize.py +++ b/id.saladeaula.digital/app/routes/authorize.py @@ -24,7 +24,7 @@ dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client) def authorize(): current_event = router.current_event cookies = parse_cookies(current_event.get('cookies', [])) - session = cookies.get('__session') + session = cookies.get('SID') if not session: raise BadRequestError('Missing session') diff --git a/id.saladeaula.digital/app/routes/session.py b/id.saladeaula.digital/app/routes/session.py deleted file mode 100644 index 1aa6011..0000000 --- a/id.saladeaula.digital/app/routes/session.py +++ /dev/null @@ -1,158 +0,0 @@ -from http import HTTPStatus -from typing import Annotated -from uuid import uuid4 - -import boto3 -from aws_lambda_powertools.event_handler import ( - Response, -) -from aws_lambda_powertools.event_handler.api_gateway import Router -from aws_lambda_powertools.event_handler.exceptions import ForbiddenError, NotFoundError -from aws_lambda_powertools.event_handler.openapi.params import Body -from aws_lambda_powertools.shared.cookies import Cookie -from layercake.dateutils import now, ttl -from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey -from passlib.hash import pbkdf2_sha256 - -from boto3clients import dynamodb_client -from config import ( - OAUTH2_TABLE, - SESSION_EXPIRES_IN, -) - -router = Router() -dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client) -idp = boto3.client('cognito-idp') - - -@router.post('/session') -def session( - username: Annotated[str, Body()], - password: Annotated[str, Body()], -): - user_id, password_hash = _get_user(username) - - if not password_hash: - _get_idp_user(user_id, username, password) - else: - if not pbkdf2_sha256.verify(password, password_hash): - raise ForbiddenError('Invalid credentials') - - return Response( - status_code=HTTPStatus.OK, - cookies=[ - Cookie( - name='__session', - value=new_session(user_id), - http_only=True, - secure=True, - same_site=None, - max_age=SESSION_EXPIRES_IN, - ) - ], - ) - - -def _get_user(username: str) -> tuple[str, str | None]: - sk = SortKey(username, path_spec='user_id') - user = dyn.collection.get_items( - KeyPair(pk='email', sk=sk, rename_key=sk.path_spec) - + KeyPair(pk='cpf', sk=sk, rename_key=sk.path_spec), - ) - - if not user: - raise UserNotFoundError() - - password = dyn.collection.get_item( - KeyPair( - pk=user['user_id'], - sk=SortKey( - sk='PASSWORD', - path_spec='hash', - rename_key='password', - ), - ), - raise_on_error=False, - default=None, - # Uncomment the following line when removing support for Cognito - # exc_cls=UserNotFoundError, - ) - - return user['user_id'], password - - -def _get_idp_user( - user_id: str, - username: str, - password: str, -) -> bool: - import base64 - import hashlib - import hmac - - # That should be removed when completing the migration - # to our own OAuth2 implementation. - client_id = '3ijacqc7r2jc9l4oli2b41f7te' - client_secret = 'amktf9l40g1mlqdo9fjlcfvpn2cp3mvh4pt97hu55sfelccos58' - - dig = hmac.new( - client_secret.encode('utf-8'), - msg=(username + client_id).encode('utf-8'), - digestmod=hashlib.sha256, - ).digest() - - try: - idp.initiate_auth( - AuthFlow='USER_PASSWORD_AUTH', - AuthParameters={ - 'USERNAME': username, - 'PASSWORD': password, - 'SECRET_HASH': base64.b64encode(dig).decode(), - }, - ClientId=client_id, - ) - - dyn.put_item( - item={ - 'id': user_id, - 'sk': 'PASSWORD', - 'hash': pbkdf2_sha256.hash(password), - 'created_at': now(), - } - ) - except Exception: - raise ForbiddenError('Invalid credentials') - - return True - - -def new_session(sub: str) -> str: - sid = str(uuid4()) - now_ = now() - exp = ttl(start_dt=now_, seconds=SESSION_EXPIRES_IN) - - with dyn.transact_writer() as transact: - transact.put( - item={ - 'id': 'SESSION', - 'sk': sid, - 'user_id': sub, - 'ttl': exp, - 'created_at': now_, - } - ) - transact.put( - item={ - 'id': sub, - 'sk': f'SESSION#{sid}', - 'ttl': exp, - 'created_at': now_, - } - ) - - return f'{sid}:{sub}' - - -class UserNotFoundError(NotFoundError): - def __init__(self, *_): - super().__init__('User not found') diff --git a/id.saladeaula.digital/template.yaml b/id.saladeaula.digital/template.yaml index 4fb57e5..f6520d6 100644 --- a/id.saladeaula.digital/template.yaml +++ b/id.saladeaula.digital/template.yaml @@ -56,10 +56,16 @@ Resources: - cognito-idp:InitiateAuth Resource: !Sub arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/* Events: - Session: + Authentication: Type: HttpApi Properties: - Path: /session + Path: /authentication + Method: POST + ApiId: !Ref HttpApi + Register: + Type: HttpApi + Properties: + Path: /register Method: POST ApiId: !Ref HttpApi Authorize: diff --git a/packages/ui/src/components/data-table/data-table.tsx b/packages/ui/src/components/data-table/data-table.tsx index bcf9020..0d18579 100644 --- a/packages/ui/src/components/data-table/data-table.tsx +++ b/packages/ui/src/components/data-table/data-table.tsx @@ -271,7 +271,10 @@ export function DataTable({ {row.getVisibleCells().map((cell) => { const isPinned = cell.column.getIsPinned() @@ -281,8 +284,13 @@ export function DataTable({ key={cell.id} className={cn( isPinned && - 'lg:sticky z-1 bg-card group-hover:bg-muted group-has-data-[state=open]:bg-muted', - isPinned && isSelected && 'bg-muted', + 'lg:sticky z-1 bg-card group-has-data-[state=open]:bg-muted', + isPinned && + !isSelected && + 'group-hover:bg-muted', + isPinned && + isSelected && + 'bg-gray-50 dark:bg-neutral-900', isPinned === 'left' && 'left-0', isPinned === 'right' && 'right-0', // Override the shadcn class diff --git a/packages/ui/src/routes/users/data.tsx b/packages/ui/src/routes/users/data.tsx index 45ca593..487f0c4 100644 --- a/packages/ui/src/routes/users/data.tsx +++ b/packages/ui/src/routes/users/data.tsx @@ -4,8 +4,7 @@ export type User = { id: string name: string email: string - cpf?: string - cnpj?: string + cpf: string } export const headers = {