feat: integração SPA de painel financeiro e autenticação global#255
feat: integração SPA de painel financeiro e autenticação global#255glauccoeng-prog wants to merge 1 commit intomate-academy:masterfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Esta PR introduz uma SPA (React/Vite) para painel financeiro (despesas/categorias) e adiciona uma camada completa de autenticação no backend (JWT em cookies, fluxo de ativação por email, reset de senha e OAuth via Passport), integrando rotas protegidas e rotas de visitante.
Changes:
- Backend: implementação de autenticação (JWT stateless + cookies), perfil do usuário, reset/ativação por email e integração OAuth (Google/GitHub/Apple).
- Backend: criação de endpoints de contabilidade (
/accounting/expensese/accounting/categories) com serviços/models Sequelize. - Frontend: nova SPA com rotas protegidas/guest, contexto global de auth, APIs para auth/expenses/categories e um conjunto de componentes UI/tema.
Reviewed changes
Copilot reviewed 114 out of 118 changed files in this pull request and generated 19 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/validatePassword.js | Validação de regras de senha |
| src/utils/ApiError.js | Erro HTTP estruturado |
| src/setup.js | Setup Sequelize/Postgres |
| src/services/userService.js | CRUD/queries User |
| src/services/tokenService.js | Geração/validação JWT |
| src/services/jwtService.js | Wrapper jsonwebtoken |
| src/services/expenses.service.js | Serviço de despesas + filtros |
| src/services/emailService.js | Envio de emails via Resend |
| src/services/categories.service.js | Serviço de categorias |
| src/services/authService.js | Fluxos de auth (register/login/refresh/reset) |
| src/routes/userRoutes.js | Rotas de perfil protegidas |
| src/routes/oauthRoutes.js | Rotas OAuth (Passport) |
| src/routes/expensesRoutes.js | Rotas REST de despesas |
| src/routes/categoriesRoutes.js | Rotas REST de categorias |
| src/routes/authRoutes.js | Rotas de autenticação |
| src/models/User.js | Model Sequelize User |
| src/models/Expense.js | Model Sequelize Expense |
| src/models/Category.js | Model Sequelize Category |
| src/middlewares/guestMiddleware.js | Proteção guest-only |
| src/middlewares/errorMiddleware.js | Handler global de erros |
| src/middlewares/authMiddleware.js | Proteção por access token cookie |
| src/index.js | Bootstrap + sync DB + start server |
| src/createServer.js | Factory do Express (CORS/session/passport/rotas) |
| src/controllers/userController.js | Controller de perfil |
| src/controllers/expenses.controller.js | Controller de despesas |
| src/controllers/categories.controller.js | Controller de categorias |
| src/controllers/authController.js | Controller de autenticação |
| src/config/passport.js | Estratégias Passport (Google/GitHub/Apple) |
| package.json | Deps backend + bump scripts |
| client/vite.config.js | Proxy /api/* para backend |
| client/src/styles/theme.css | Variáveis/tema (dark/light) |
| client/src/styles/tailwind.css | Imports Tailwind |
| client/src/styles/index.css | Entry CSS |
| client/src/styles/fonts.css | Fonte Inter (Google Fonts) |
| client/src/main.jsx | Bootstrap React root |
| client/src/context/useAuth.js | Hook de auth context |
| client/src/context/authContextValue.js | Context value isolado |
| client/src/context/AuthContext.jsx | AuthProvider (refresh/login/logout/etc) |
| client/src/app/components/ui/utils.ts | cn helper (clsx + twMerge) |
| client/src/app/components/ui/use-mobile.ts | Hook breakpoint mobile |
| client/src/app/components/ui/tooltip.tsx | Tooltip (Radix) |
| client/src/app/components/ui/toggle.tsx | Toggle (Radix) |
| client/src/app/components/ui/toggle-group.tsx | ToggleGroup (Radix) |
| client/src/app/components/ui/textarea.tsx | Textarea UI |
| client/src/app/components/ui/tabs.tsx | Tabs UI |
| client/src/app/components/ui/table.tsx | Table UI |
| client/src/app/components/ui/switch.tsx | Switch UI |
| client/src/app/components/ui/sonner.tsx | Toaster wrapper (Sonner) |
| client/src/app/components/ui/slider.tsx | Slider UI |
| client/src/app/components/ui/skeleton.tsx | Skeleton UI |
| client/src/app/components/ui/sheet.tsx | Sheet/Drawer UI (Dialog) |
| client/src/app/components/ui/separator.tsx | Separator UI |
| client/src/app/components/ui/select.tsx | Select UI |
| client/src/app/components/ui/scroll-area.tsx | ScrollArea UI |
| client/src/app/components/ui/resizable.tsx | Resizable panels UI |
| client/src/app/components/ui/radio-group.tsx | RadioGroup UI |
| client/src/app/components/ui/progress.tsx | Progress UI |
| client/src/app/components/ui/popover.tsx | Popover UI |
| client/src/app/components/ui/pagination.tsx | Pagination UI |
| client/src/app/components/ui/navigation-menu.tsx | NavigationMenu UI |
| client/src/app/components/ui/label.tsx | Label UI |
| client/src/app/components/ui/input.tsx | Input UI |
| client/src/app/components/ui/input-otp.tsx | OTP input UI |
| client/src/app/components/ui/hover-card.tsx | HoverCard UI |
| client/src/app/components/ui/form.tsx | RHF form primitives |
| client/src/app/components/ui/drawer.tsx | Drawer (Vaul) UI |
| client/src/app/components/ui/dialog.tsx | Dialog UI |
| client/src/app/components/ui/context-menu.tsx | ContextMenu UI |
| client/src/app/components/ui/command.tsx | Command palette UI |
| client/src/app/components/ui/collapsible.tsx | Collapsible UI |
| client/src/app/components/ui/checkbox.tsx | Checkbox UI |
| client/src/app/components/ui/carousel.tsx | Carousel UI (Embla) |
| client/src/app/components/ui/card.tsx | Card UI |
| client/src/app/components/ui/calendar.tsx | Calendar UI |
| client/src/app/components/ui/button.tsx | Button UI |
| client/src/app/components/ui/breadcrumb.tsx | Breadcrumb UI |
| client/src/app/components/ui/badge.tsx | Badge UI |
| client/src/app/components/ui/avatar.tsx | Avatar UI |
| client/src/app/components/ui/aspect-ratio.tsx | AspectRatio UI |
| client/src/app/components/ui/alert.tsx | Alert UI |
| client/src/app/components/ui/alert-dialog.tsx | AlertDialog UI |
| client/src/app/components/ui/accordion.tsx | Accordion UI |
| client/src/app/components/theme-toggle.tsx | Toggle light/dark |
| client/src/app/components/theme-provider.tsx | Provider de tema (custom) |
| client/src/app/components/reset-password-page.tsx | Tela solicitar reset |
| client/src/app/components/reset-confirm-page.tsx | Tela confirmar reset |
| client/src/app/components/register-page.tsx | Tela de registro |
| client/src/app/components/protected-route.tsx | Guard rota protegida |
| client/src/app/components/profile-page.tsx | Página de perfil |
| client/src/app/components/not-found-page.tsx | 404 page |
| client/src/app/components/guest-route.tsx | Guard rota guest |
| client/src/app/components/categories-page.tsx | Página categorias |
| client/src/app/components/auth-layout.tsx | Layout telas auth |
| client/src/app/components/app-layout.tsx | Layout app (sidebar/topbar) |
| client/src/app/components/activation-page.tsx | Tela ativação conta |
| client/src/api/expenseApi.ts | Client API despesas |
| client/src/api/categoryApi.ts | Client API categorias |
| client/src/api/authApi.js | Client API auth/perfil |
| client/src/App.jsx | Rotas SPA + providers |
| client/public/favicon.svg | Favicon |
| client/package.json | Deps/scripts do client |
| client/jsconfig.json | Config TS/paths (JS) |
| client/index.html | HTML Vite |
| client/eslint.config.js | ESLint config client |
| client/.gitignore | Gitignore client |
| .gitignore | Ignora .env etc |
| .github/workflows/test.yml-template | Template CI |
| .eslintrc.js | Ajustes ESLint root |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const create = async (req, res) => { | ||
| const { name, type } = req.body; | ||
|
|
||
| // Validação básica: nome não pode ser vazio | ||
| if (!name) { | ||
| return res.status(400).json({ message: 'Nome é obrigatório' }); | ||
| } | ||
|
|
||
| const newCategory = await categoriesService.create({ | ||
| name, | ||
| type, | ||
| userId: req.user.id, | ||
| }); |
There was a problem hiding this comment.
O controller envia type para categoriesService.create, mas o modelo Category não define a coluna type. Dependendo da config do Sequelize, isso pode ser ignorado ou causar erro, e o cliente espera type na resposta. Sugestão: ou adicionar type ao model/migration, ou remover type do payload/contrato da API.
| // Verifica a senha atual | ||
| const isOldPasswordValid = await bcrypt.compare(oldPassword, user.password); | ||
|
|
||
| if (!isOldPasswordValid) { | ||
| throw ApiError.badRequest('Senha atual incorreta'); | ||
| } |
There was a problem hiding this comment.
bcrypt.compare(oldPassword, user.password) vai lançar erro se user.password for null (contas OAuth). Sugestão: antes do compare, bloquear a ação para contas sem senha (retornar 400/409) ou implementar fluxo de “definir senha” para OAuth.
| app.use( | ||
| session({ | ||
| secret: process.env.SESSION_SECRET || 'session_secret', | ||
| resave: false, | ||
| saveUninitialized: false, | ||
| cookie: { | ||
| secure: false, // true em produção com HTTPS | ||
| maxAge: 24 * 60 * 60 * 1000, // 24 horas | ||
| }, | ||
| }), |
There was a problem hiding this comment.
A configuração de sessão usa fallback session_secret e cookie.secure: false. Em produção isso é inseguro (secret previsível e cookie de sessão pode trafegar em HTTP). Sugestão: exigir SESSION_SECRET e setar cookie.secure/sameSite/proxy corretamente quando NODE_ENV==='production' (e considerar trust proxy).
| import { useTheme } from "next-themes"; | ||
| import { Toaster as Sonner, ToasterProps } from "sonner"; | ||
|
|
||
| const Toaster = ({ ...props }: ToasterProps) => { | ||
| const { theme = "system" } = useTheme(); | ||
|
|
There was a problem hiding this comment.
Este componente usa useTheme de next-themes, mas a aplicação está usando um ThemeProvider próprio (client/src/app/components/theme-provider.tsx). Isso deve causar erro de runtime ou tema não funcionar. Sugestão: trocar para o hook/context do provider local, ou envolver a app com o ThemeProvider do next-themes consistentemente.
| // Sincroniza os modelos com o banco de dados | ||
| // alter: true atualiza as tabelas existentes sem perder dados | ||
| await sequelize.sync({ alter: true }); | ||
|
|
There was a problem hiding this comment.
sequelize.sync({ alter: true }) em runtime é perigoso (pode gerar alterações de schema inesperadas e travar/romper produção). Sugestão: usar migrations e/ou condicionar alter apenas a ambiente de desenvolvimento (ex.: NODE_ENV !== 'production').
| import { cn } from "./utils"; | ||
|
|
||
| function Skeleton({ className, ...props }: React.ComponentProps<"div">) { | ||
| return ( |
There was a problem hiding this comment.
Este arquivo usa React.ComponentProps mas não importa React (nem type import). Em TS isso normalmente quebra com Cannot find name 'React'. Sugestão: adicionar import type * as React from 'react' (ou importar ComponentProps diretamente).
| style={ | ||
| { | ||
| "--normal-bg": "var(--popover)", | ||
| "--normal-text": "var(--popover-foreground)", | ||
| "--normal-border": "var(--border)", | ||
| } as React.CSSProperties | ||
| } |
There was a problem hiding this comment.
O style faz cast para React.CSSProperties, mas não há import do namespace React no arquivo. Isso tende a causar erro de TypeScript. Sugestão: importar type { CSSProperties } de react e usar as CSSProperties (ou adicionar import type * as React from 'react').
| // Validação: campos obrigatórios | ||
| if (!spentAt || !title || !amount) { | ||
| return res | ||
| .status(400) | ||
| .json({ message: 'Todos os campos são obrigatórios' }); | ||
| } |
There was a problem hiding this comment.
A validação if (!spentAt || !title || !amount) trata amount = 0 como inválido. Se 0 for um valor permitido (ex.: ajuste/estorno), isso vai bloquear requests. Sugestão: validar amount == null (null/undefined) e/ou Number.isFinite(amount) em vez de checagem truthy.
| // Verifica a senha | ||
| const isPasswordValid = await bcrypt.compare(password, user.password); | ||
|
|
||
| if (!isPasswordValid) { | ||
| throw ApiError.badRequest('Senha incorreta'); | ||
| } |
There was a problem hiding this comment.
bcrypt.compare(password, user.password) também falha para usuários OAuth (user.password null). Além disso, mudança de email via senha não funciona para esse tipo de conta. Sugestão: validar user.password antes e retornar erro claro/instrução (ex.: “conta social, defina uma senha primeiro”).
| id: number; | ||
| userId: number; | ||
| name: string; | ||
| type: string; |
There was a problem hiding this comment.
O client tipa/serializa CategoryResponse com campo obrigatório type, mas o backend (model/controller) não garante esse campo na resposta. Isso pode causar inconsistências/erros de runtime. Sugestão: alinhar o contrato (remover type do client ou adicionar suporte completo no backend).
| type: string; | |
| type?: string; |
No description provided.