From 98e15b816ed94e785934bbd4b8cb9aa7478d450a Mon Sep 17 00:00:00 2001 From: rever-tecnologia Date: Mon, 6 Oct 2025 10:25:08 -0300 Subject: [PATCH] feat: improve auth seeding and sidebar ux Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- agents.md | 15 ++ web/scripts/seed-agents.mjs | 138 ++++++++++++++++++ .../admin/queues/queues-manager.tsx | 16 +- web/src/components/app-sidebar.tsx | 63 ++++++-- web/src/components/ui/dialog.tsx | 8 + 5 files changed, 226 insertions(+), 14 deletions(-) create mode 100644 web/scripts/seed-agents.mjs diff --git a/agents.md b/agents.md index 162fb24..5bed7d3 100644 --- a/agents.md +++ b/agents.md @@ -5,6 +5,21 @@ Construir o nucleo de tickets compartilhado entre web e desktop (Tauri), garanti ### Contato principal - **Esdras Renan** — monkeyesdras@gmail.com + +### Credenciais seed (ambiente local) +- Administrador padrão: `admin@sistema.dev` / `admin123` +- Agentes carregados via seed (senha inicial `agent123`, altere após o primeiro acesso): + - Gabriel Oliveira — gabriel.oliveira@rever.com.br + - George Araujo — george.araujo@rever.com.br + - Hugo Soares — hugo.soares@rever.com.br + - Julio Cesar — julio@rever.com.br + - Lorena Magalhães — lorena@rever.com.br + - Rever — renan.pac@paulicon.com.br + - Telão — suporte@rever.com.br + - Thiago Medeiros — thiago.medeiros@rever.com.br + - Weslei Magalhães — weslei@rever.com.br + +> Observação: todos os usuários acima foram sincronizados com o Convex. Atualize as senhas imediatamente após o primeiro login. ## Fase A - Fundamentos da plataforma 1. **Scaffold e DX** diff --git a/web/scripts/seed-agents.mjs b/web/scripts/seed-agents.mjs new file mode 100644 index 0000000..5e0732f --- /dev/null +++ b/web/scripts/seed-agents.mjs @@ -0,0 +1,138 @@ +import pkg from "@prisma/client" +import { hashPassword } from "better-auth/crypto" +import { ConvexHttpClient } from "convex/browser" + +const { PrismaClient } = pkg +const prisma = new PrismaClient() + +const USERS = [ + { name: "Administrador", email: "admin@sistema.dev", role: "admin" }, + { name: "Gabriel Oliveira", email: "gabriel.oliveira@rever.com.br", role: "agent" }, + { name: "George Araujo", email: "george.araujo@rever.com.br", role: "agent" }, + { name: "Hugo Soares", email: "hugo.soares@rever.com.br", role: "agent" }, + { name: "Julio Cesar", email: "julio@rever.com.br", role: "agent" }, + { name: "Lorena Magalhães", email: "lorena@rever.com.br", role: "agent" }, + { name: "Rever", email: "renan.pac@paulicon.com.br", role: "agent" }, + { name: "Telão", email: "suporte@rever.com.br", role: "agent" }, + { name: "Thiago Medeiros", email: "thiago.medeiros@rever.com.br", role: "agent" }, + { name: "Weslei Magalhães", email: "weslei@rever.com.br", role: "agent" }, +] + +const TENANT_ID = process.env.SEED_TENANT_ID ?? "tenant-atlas" +const DEFAULT_AGENT_PASSWORD = process.env.SEED_AGENT_PASSWORD ?? "agent123" +const DEFAULT_ADMIN_PASSWORD = process.env.SEED_ADMIN_PASSWORD ?? "admin123" +const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL + +async function syncConvexUsers(users) { + if (!CONVEX_URL) { + console.warn("NEXT_PUBLIC_CONVEX_URL não definido; sincronização com Convex ignorada.") + return + } + + const client = new ConvexHttpClient(CONVEX_URL) + for (const user of users) { + try { + await client.mutation("users:ensureUser", { + tenantId: TENANT_ID, + email: user.email, + name: user.name, + role: user.role.toUpperCase(), + }) + } catch (error) { + console.error(`Falha ao sincronizar usuário ${user.email} com Convex`, error) + } + } +} + +async function main() { + const emails = USERS.map((user) => user.email.toLowerCase()) + + const existing = await prisma.authUser.findMany({ + where: { + email: { + notIn: emails, + }, + }, + select: { id: true }, + }) + + if (existing.length > 0) { + const ids = existing.map((user) => user.id) + await prisma.authSession.deleteMany({ where: { userId: { in: ids } } }) + await prisma.authAccount.deleteMany({ where: { userId: { in: ids } } }) + await prisma.authUser.deleteMany({ where: { id: { in: ids } } }) + } + + const seededUsers = [] + + for (const definition of USERS) { + const email = definition.email.toLowerCase() + const role = definition.role ?? "agent" + const password = definition.password ?? (role === "admin" ? DEFAULT_ADMIN_PASSWORD : DEFAULT_AGENT_PASSWORD) + const hashedPassword = await hashPassword(password) + + const user = await prisma.authUser.upsert({ + where: { email }, + update: { + name: definition.name, + role, + tenantId: TENANT_ID, + emailVerified: true, + }, + create: { + email, + name: definition.name, + role, + tenantId: TENANT_ID, + emailVerified: true, + accounts: { + create: { + providerId: "credential", + accountId: email, + password: hashedPassword, + }, + }, + }, + include: { accounts: true }, + }) + + const credentialAccount = user.accounts.find( + (account) => account.providerId === "credential" && account.accountId === email, + ) + + if (credentialAccount) { + await prisma.authAccount.update({ + where: { id: credentialAccount.id }, + data: { password: hashedPassword }, + }) + } else { + await prisma.authAccount.create({ + data: { + userId: user.id, + providerId: "credential", + accountId: email, + password: hashedPassword, + }, + }) + } + + seededUsers.push({ id: user.id, name: definition.name, email, role }) + console.log(`✅ Usuário sincronizado: ${definition.name} <${email}> (${role})`) + } + + await syncConvexUsers(seededUsers) + + console.log("") + console.log(`Senha padrão agentes: ${DEFAULT_AGENT_PASSWORD}`) + console.log(`Senha padrão administrador: ${DEFAULT_ADMIN_PASSWORD}`) + console.log(`Total de usuários ativos: ${seededUsers.length}`) +} + +main() + .catch((error) => { + console.error("Erro ao processar agentes", error) + process.exitCode = 1 + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/web/src/components/admin/queues/queues-manager.tsx b/web/src/components/admin/queues/queues-manager.tsx index de766ac..9b34cbf 100644 --- a/web/src/components/admin/queues/queues-manager.tsx +++ b/web/src/components/admin/queues/queues-manager.tsx @@ -34,6 +34,8 @@ export function QueuesManager() { const { session, convexUserId } = useAuth() const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID + const NO_TEAM_VALUE = "__none__" + const queues = useQuery( api.queues.list, convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip" @@ -202,12 +204,15 @@ export function QueuesManager() {
- setTeamId(value === NO_TEAM_VALUE ? undefined : value)} + > - Sem time + Sem time {teams?.map((team) => ( {team.name} @@ -292,12 +297,15 @@ export function QueuesManager() {
- setTeamId(value === NO_TEAM_VALUE ? undefined : value)} + > - Sem time + Sem time {teams?.map((team) => ( {team.name} diff --git a/web/src/components/app-sidebar.tsx b/web/src/components/app-sidebar.tsx index 87fb3d8..8563f66 100644 --- a/web/src/components/app-sidebar.tsx +++ b/web/src/components/app-sidebar.tsx @@ -13,7 +13,6 @@ import { Users, Waypoints, Timer, - Plug, Layers3, UserPlus, Settings, @@ -34,6 +33,7 @@ import { SidebarMenuItem, SidebarRail, } from "@/components/ui/sidebar" +import { Skeleton } from "@/components/ui/skeleton" import { useAuth } from "@/lib/auth-client" import type { LucideIcon } from "lucide-react" @@ -45,6 +45,7 @@ type NavigationItem = { url: string icon: LucideIcon requiredRole?: NavRoleRequirement + exact?: boolean } type NavigationGroup = { @@ -79,12 +80,17 @@ const navigation: { versions: string[]; navMain: NavigationGroup[] } = { title: "Administração", requiredRole: "admin", items: [ - { title: "Convites e acessos", url: "/admin", icon: UserPlus, requiredRole: "admin" }, + { + title: "Convites e acessos", + url: "/admin", + icon: UserPlus, + requiredRole: "admin", + exact: true, + }, { title: "Canais & roteamento", url: "/admin/channels", icon: Waypoints, requiredRole: "admin" }, { title: "Times & papéis", url: "/admin/teams", icon: Users, requiredRole: "admin" }, { title: "Campos personalizados", url: "/admin/fields", icon: Layers3, requiredRole: "admin" }, { title: "SLAs", url: "/admin/slas", icon: Timer, requiredRole: "admin" }, - { title: "Integrações", url: "/admin/integrations", icon: Plug, requiredRole: "admin" }, ], }, { @@ -95,16 +101,25 @@ const navigation: { versions: string[]; navMain: NavigationGroup[] } = { ], } -export function AppSidebar({ ...props }: React.ComponentProps) { +export function AppSidebar({ ...props }: React.ComponentProps) { const pathname = usePathname() const { isAdmin, isStaff, isCustomer } = useAuth() + const [isHydrated, setIsHydrated] = React.useState(false) + + React.useEffect(() => { + setIsHydrated(true) + }, []) - function isActive(url: string) { + function isActive(item: NavigationItem) { + const { url, exact } = item if (!pathname) return false - if (url === "/dashboard" && pathname === "/") { + if (url === "/dashboard" && pathname === "/") { return true } - return pathname === url || pathname.startsWith(`${url}/`) + if (exact) { + return pathname === url + } + return pathname === url || pathname.startsWith(`${url}/`) } function canAccess(requiredRole?: NavRoleRequirement) { @@ -115,7 +130,35 @@ export function AppSidebar({ ...props }: React.ComponentProps) { return false } - return ( + if (!isHydrated) { + return ( + + + + + + + {[0, 1, 2].map((group) => ( + + + + + +
+ {[0, 1, 2].map((item) => ( + + ))} +
+
+
+ ))} +
+ +
+ ) + } + + return ( ) { versions={[...navigation.versions]} defaultVersion={navigation.versions[0]} /> - + {navigation.navMain.map((group) => { @@ -137,7 +180,7 @@ export function AppSidebar({ ...props }: React.ComponentProps) { {visibleItems.map((item) => ( - + {item.title} diff --git a/web/src/components/ui/dialog.tsx b/web/src/components/ui/dialog.tsx index 1f7d14c..953f594 100644 --- a/web/src/components/ui/dialog.tsx +++ b/web/src/components/ui/dialog.tsx @@ -47,6 +47,13 @@ const DialogHeader = ({ className, ...props }: React.HTMLAttributes ) +const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+) + const DialogTitle = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef @@ -64,4 +71,5 @@ const DialogDescription = React.forwardRef< DialogDescription.displayName = DialogPrimitive.Description.displayName export { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription } +export { DialogFooter }