From 70043646c323ea4a36fbd8cb528907e52b89d683 Mon Sep 17 00:00:00 2001 From: Benevanio Santos Date: Tue, 9 Jun 2026 22:48:07 -0300 Subject: [PATCH 1/3] feat(auth): PAV-51 cria authService com funcoes de autenticacao --- frontend/index.html | 5 +- frontend/public/icone.png | Bin 0 -> 5428 bytes frontend/src/components/login/LeftSide.tsx | 6 +- .../src/components/login/RegisterSide.tsx | 107 ++++-- frontend/src/components/login/RigthSide.tsx | 70 +++- frontend/src/services/authService.ts | 187 +++++++++ .../components/login/RegisterSide.test.tsx | 229 +++++++++-- .../unit/components/login/RigthSide.test.tsx | 161 ++++++-- .../tests/unit/services/auth.service.test.tsx | 359 ++++++++++++++++++ frontend/vite.config.js | 24 +- package-lock.json | 2 +- package.json | 2 +- 12 files changed, 1026 insertions(+), 126 deletions(-) create mode 100644 frontend/public/icone.png create mode 100644 frontend/src/services/authService.ts create mode 100644 frontend/tests/unit/services/auth.service.test.tsx diff --git a/frontend/index.html b/frontend/index.html index e0b3e5d..12ad152 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,8 +2,7 @@ - - + - Painel de Vagas + <Cand!Date!>
diff --git a/frontend/public/icone.png b/frontend/public/icone.png new file mode 100644 index 0000000000000000000000000000000000000000..d61420db45594e536e59218ab3d6a973c5a5d5d6 GIT binary patch literal 5428 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 e?FB_20RA6(M;vmn@qg0*0000
@@ -77,4 +77,4 @@ export default function LeftSide() { ) -} +} \ No newline at end of file diff --git a/frontend/src/components/login/RegisterSide.tsx b/frontend/src/components/login/RegisterSide.tsx index 71eb5d8..8a53386 100644 --- a/frontend/src/components/login/RegisterSide.tsx +++ b/frontend/src/components/login/RegisterSide.tsx @@ -1,7 +1,9 @@ -import { useState, FormEvent } from "react"; -import { Eye, EyeOff, ArrowLeft } from "lucide-react"; +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { register } from "@/services/authService"; // ← MUDE para register import { Image } from "@unpic/react"; import { motion } from "framer-motion"; +import { ArrowLeft, Eye, EyeOff } from "lucide-react"; +import { FormEvent, useState } from "react"; import PhoneInput from "react-phone-number-input"; import "react-phone-number-input/style.css"; @@ -59,6 +61,9 @@ export default function RegisterSide() { const [telefoneError, setTelefoneError] = useState(""); const [passwordError, setPasswordError] = useState(""); const [cpfError, setCpfError] = useState(""); + + const [isLoading, setIsLoading] = useState(false); + const [apiError, setApiError] = useState(""); const handleRevealPassword = () => setShowPassword((prev) => !prev); @@ -70,41 +75,67 @@ export default function RegisterSide() { return `${digits.slice(0, 3)}.${digits.slice(3, 6)}.${digits.slice(6, 9)}-${digits.slice(9)}`; }; - const handleSubmit = (e: FormEvent) => { + const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setNomeError(""); setEmailError(""); setTelefoneError(""); setPasswordError(""); setCpfError(""); + setApiError(""); let isValid = true; - if (!nome) { setNomeError("O campo de nome é obrigatório."); isValid = false; } + if (!nome) { + setNomeError("O campo de nome é obrigatório."); + isValid = false; + } if (!email) { - setEmailError("O campo de e-mail é obrigatório."); isValid = false; + setEmailError("O campo de e-mail é obrigatório."); + isValid = false; } else { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(email)) { setEmailError("Por favor, insira um e-mail válido."); isValid = false; } + if (!emailRegex.test(email)) { + setEmailError("Por favor, insira um e-mail válido."); + isValid = false; + } } - if (!telefone) { setTelefoneError("O campo de telefone é obrigatório."); isValid = false; } + if (!telefone) { + setTelefoneError("O campo de telefone é obrigatório."); + isValid = false; + } if (!password) { - setPasswordError("O campo de senha é obrigatório."); isValid = false; + setPasswordError("O campo de senha é obrigatório."); + isValid = false; } else if (password.length < 6) { - setPasswordError("A senha precisa conter pelo menos 6 caracteres."); isValid = false; + setPasswordError("A senha precisa conter pelo menos 6 caracteres."); + isValid = false; } - if (!cpf) { - setCpfError("O campo de CPF é obrigatório."); isValid = false; - } else if (cpf.replace(/\D/g, "").length < 11) { - setCpfError("Por favor, insira um CPF válido."); isValid = false; + if (cpf && cpf.replace(/\D/g, "").length > 0 && cpf.replace(/\D/g, "").length !== 11) { + setCpfError("CPF inválido. Deve conter 11 dígitos."); + isValid = false; } if (isValid) { - console.log("Cadastro válido!", { nome, email, telefone, password, cpf }); + setIsLoading(true); + try { + const result = await register({ + email: email, + password: password, + name: nome, + }); + console.log("Cadastro bem sucedido:", result); + window.location.href = "/login?registered=true"; + } catch (error: any) { + console.error("Erro no cadastro:", error); + setApiError(error.message || "Erro ao cadastrar. Tente novamente."); + } finally { + setIsLoading(false); + } } }; @@ -144,6 +175,12 @@ export default function RegisterSide() {
+ {apiError && ( +
+

{apiError}

+
+ )} +
@@ -170,10 +208,11 @@ export default function RegisterSide() { type="email" value={email} onChange={(e) => setEmail(e.target.value)} - placeholder="lbene17@gmail.com" + placeholder="benevanio@dev.com.br" + disabled={isLoading} className={`w-full px-4 py-3.5 rounded-xl border bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 transition-all shadow-sm ${ emailError ? "border-red-500 focus:ring-red-500" : "border-gray-200 dark:border-neutral-700 focus:ring-blue-500 focus:border-transparent" - }`} + } ${isLoading ? "opacity-50 cursor-not-allowed" : ""}`} /> {emailError &&

{emailError}

} @@ -184,12 +223,13 @@ export default function RegisterSide() {
+ } ${isLoading ? "opacity-50" : ""}`}> @@ -208,13 +248,15 @@ export default function RegisterSide() { value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Ex: ••••••••••••" + disabled={isLoading} className={`w-full px-4 py-3.5 rounded-xl border bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 transition-all shadow-sm ${ passwordError ? "border-red-500 focus:ring-red-500" : "border-gray-200 dark:border-neutral-700 focus:ring-blue-500 focus:border-transparent" - }`} + } ${isLoading ? "opacity-50 cursor-not-allowed" : ""}`} />
- {/* Botões Sociais Atualizados com Hover translúcido e bordas combinando */}
- - -
+ + {apiError && ( +
+

{apiError}

+
+ )} +
@@ -152,15 +176,17 @@ export default function RightSide() { value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Ex: ••••••••••••" + disabled={isLoading} className={`w-full px-4 py-3.5 rounded-xl border bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 transition-all shadow-sm ${ passwordError ? "border-red-500 focus:ring-red-500" : "border-gray-200 dark:border-neutral-700 focus:ring-blue-500 focus:border-transparent" - }`} + } ${isLoading ? "opacity-50 cursor-not-allowed" : ""}`} />
- - , + button: ({ children, ...props }: any) => ( + + ), }, })); vi.mock("react-phone-number-input", () => ({ - default: ({ value, onChange, placeholder }: any) => ( + default: ({ value, onChange, placeholder, disabled }: any) => ( onChange(e.target.value)} placeholder={placeholder} - onChange={(event) => onChange(event.target.value)} + disabled={disabled} /> ), })); import RegisterSide from "@/components/login/RegisterSide"; +// ===================== +// TEST SUITE +// ===================== describe("RegisterSide", () => { - const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const originalLocation = window.location; beforeEach(() => { - logSpy.mockClear(); + mockRegister.mockReset(); + + Object.defineProperty(window, "location", { + configurable: true, + value: { href: "" }, + }); + }); + + afterEach(() => { + Object.defineProperty(window, "location", { + configurable: true, + value: originalLocation, + }); }); - it("renderiza formulário completo de cadastro", () => { + it("renderiza formulário e alterna visibilidade da senha", () => { render(); - expect(screen.getByLabelText(/nome/i)).toBeInTheDocument(); - expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); - expect(screen.getByLabelText(/^telefone$/i)).toBeInTheDocument(); - expect(screen.getByLabelText(/senha/i)).toBeInTheDocument(); - expect(screen.getByLabelText(/cpf/i)).toBeInTheDocument(); + const passwordInput = screen.getByLabelText(/senha/i) as HTMLInputElement; + expect(passwordInput.type).toBe("password"); + + const toggleButtons = screen.getAllByRole("button"); + fireEvent.click(toggleButtons[0]); + + expect(screen.getByLabelText(/senha/i)).toHaveAttribute("type", "text"); }); - it("mostra erros obrigatórios quando submit vazio", () => { + it("mostra erros obrigatórios quando submit vazio", async () => { + render(); + + fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); + + expect( + await screen.findByText(/campo de nome.*obrigatório/i) + ).toBeInTheDocument(); + expect( + await screen.findByText(/campo de e-mail.*obrigatório/i) + ).toBeInTheDocument(); + expect( + await screen.findByText(/campo de telefone.*obrigatório/i) + ).toBeInTheDocument(); + expect( + await screen.findByText(/campo de senha.*obrigatório/i) + ).toBeInTheDocument(); + // CPF é campo opcional — não dispara erro de obrigatório + }); + + + it("valida CPF quando preenchido incorretamente", async () => { + render(); + + fireEvent.change(screen.getByLabelText(/nome/i), { + target: { value: "Usuário Teste" }, + }); + + fireEvent.change(screen.getByLabelText(/email/i), { + target: { value: "teste@email.com" }, + }); + + fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { + target: { value: "+5534999999999" }, + }); + + fireEvent.change(screen.getByLabelText(/senha/i), { + target: { value: "123456" }, + }); + + fireEvent.change(screen.getByLabelText(/cpf/i), { + target: { value: "123" }, + }); + + fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); + + // Componente exibe: "CPF inválido. Deve conter 11 dígitos." + expect(await screen.findByText(/cpf inv[aá]lido/i)).toBeInTheDocument(); + }); + + it("envia formulário válido com CPF opcional", async () => { + mockRegister.mockResolvedValueOnce({ + message: "Usuário criado", + user: { id: 1 }, + }); + render(); + fireEvent.change(screen.getByLabelText(/nome/i), { + target: { value: "Usuário Teste" }, + }); + + fireEvent.change(screen.getByLabelText(/email/i), { + target: { value: "teste@email.com" }, + }); + + fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { + target: { value: "+5534999999999" }, + }); + + fireEvent.change(screen.getByLabelText(/senha/i), { + target: { value: "123456" }, + }); + fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); - expect(screen.getByText(/campo de nome.*obrigatório/i)).toBeInTheDocument(); - expect(screen.getByText(/campo de e-mail.*obrigatório/i)).toBeInTheDocument(); - expect(screen.getByText(/campo de telefone.*obrigatório/i)).toBeInTheDocument(); - expect(screen.getByText(/campo de senha.*obrigatório/i)).toBeInTheDocument(); - expect(screen.getByText(/campo de cpf.*obrigatório/i)).toBeInTheDocument(); + await waitFor(() => { + expect(mockRegister).toHaveBeenCalled(); + }); }); - it("valida email, senha e cpf inválidos", () => { + it("exibe erro quando API falha", async () => { + mockRegister.mockRejectedValueOnce(new Error("Email já cadastrado")); + render(); - fireEvent.change(screen.getByLabelText(/nome/i), { target: { value: "Bene" } }); - fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "bene@teste" } }); - fireEvent.change(screen.getByLabelText(/^telefone$/i), { target: { value: "+5534999999999" } }); - fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "123" } }); - fireEvent.change(screen.getByLabelText(/cpf/i), { target: { value: "123" } }); + fireEvent.change(screen.getByLabelText(/nome/i), { + target: { value: "Bene" }, + }); + + fireEvent.change(screen.getByLabelText(/email/i), { + target: { value: "bene@teste.com" }, + }); + + fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { + target: { value: "+5534999999999" }, + }); + + fireEvent.change(screen.getByLabelText(/senha/i), { + target: { value: "123456" }, + }); fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); - expect(screen.getByText(/insira um e-mail válido/i)).toBeInTheDocument(); - expect(screen.getByText(/pelo menos 6 caracteres/i)).toBeInTheDocument(); - expect(screen.getByText(/insira um cpf válido/i)).toBeInTheDocument(); + expect( + await screen.findByText(/email já cadastrado/i) + ).toBeInTheDocument(); }); - it("formata cpf e envia submit válido", () => { + it("mostra loading durante requisição", async () => { + mockRegister.mockImplementation( + () => new Promise((resolve) => setTimeout(resolve, 200)) + ); + render(); - fireEvent.change(screen.getByLabelText(/nome/i), { target: { value: "Bene" } }); - fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "bene@teste.com" } }); - fireEvent.change(screen.getByLabelText(/^telefone$/i), { target: { value: "+5534999999999" } }); - fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "123456" } }); - fireEvent.change(screen.getByLabelText(/cpf/i), { target: { value: "12345678901" } }); + fireEvent.change(screen.getByLabelText(/nome/i), { + target: { value: "Bene" }, + }); - expect(screen.getByLabelText(/cpf/i)).toHaveValue("123.456.789-01"); + fireEvent.change(screen.getByLabelText(/email/i), { + target: { value: "bene@teste.com" }, + }); + + fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { + target: { value: "+5534999999999" }, + }); + + fireEvent.change(screen.getByLabelText(/senha/i), { + target: { value: "123456" }, + }); fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); - expect(logSpy).toHaveBeenCalledWith("Cadastro válido!", { - nome: "Bene", - email: "bene@teste.com", - telefone: "+5534999999999", - password: "123456", - cpf: "123.456.789-01", + expect( + await screen.findByRole("button", { name: /cadastrando/i }) + ).toBeDisabled(); + }); + + it("desabilita inputs durante loading", async () => { + mockRegister.mockImplementation( + () => new Promise((resolve) => setTimeout(resolve, 200)) + ); + + render(); + + const nome = screen.getByLabelText(/nome/i); + const email = screen.getByLabelText(/email/i); + const senha = screen.getByLabelText(/senha/i); + + fireEvent.change(nome, { target: { value: "Bene" } }); + fireEvent.change(email, { target: { value: "bene@teste.com" } }); + fireEvent.change(senha, { target: { value: "123456" } }); + + fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { + target: { value: "+5534999999999" }, }); + + fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); + + expect( + await screen.findByRole("button", { name: /cadastrando/i }) + ).toBeDisabled(); }); }); diff --git a/frontend/tests/unit/components/login/RigthSide.test.tsx b/frontend/tests/unit/components/login/RigthSide.test.tsx index fd852de..6658157 100644 --- a/frontend/tests/unit/components/login/RigthSide.test.tsx +++ b/frontend/tests/unit/components/login/RigthSide.test.tsx @@ -1,6 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { fireEvent, render, screen } from "@testing-library/react"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const mockLogin = vi.fn(); + +vi.mock("@/services/authService", () => ({ + login: (...args: any[]) => mockLogin(...args), +})); vi.mock("@unpic/react", () => ({ Image: (props: any) => {props.alt}, @@ -16,54 +22,163 @@ vi.mock("framer-motion", () => ({ import RigthSide from "@/components/login/RigthSide"; describe("RigthSide", () => { - const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const originalLocation = window.location; beforeEach(() => { - logSpy.mockClear(); + mockLogin.mockReset(); + Object.defineProperty(window, "location", { + configurable: true, + value: { href: "" }, + }); + localStorage.clear(); + }); + + afterEach(() => { + Object.defineProperty(window, "location", { + configurable: true, + value: originalLocation, + }); + }); + + it("renderiza formulário corretamente", () => { + render(); + + expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/senha/i)).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /entrar/i })).toBeInTheDocument(); + expect(screen.getByText(/cadastre-se/i)).toBeInTheDocument(); + expect(screen.getByText(/ou faça login com/i)).toBeInTheDocument(); }); - it("renderiza formulário e alterna visibilidade da senha", () => { + it("alterna visibilidade da senha ao clicar no botão", () => { render(); const passwordInput = screen.getByLabelText(/senha/i) as HTMLInputElement; expect(passwordInput.type).toBe("password"); - const revealButtons = screen.getAllByRole("button", { name: "" }); - fireEvent.click(revealButtons[0]); + const toggleButtons = screen.getAllByRole("button", { name: "" }); + const eyeButton = toggleButtons.find(btn => btn.querySelector('svg')); + if (eyeButton) fireEvent.click(eyeButton); - expect((screen.getByLabelText(/senha/i) as HTMLInputElement).type).toBe("text"); + expect(screen.getByLabelText(/senha/i)).toHaveAttribute("type", "text"); }); - it("mostra erros para submit invalido", () => { + it("mostra erro de email obrigatório ao submeter vazio", async () => { render(); - + fireEvent.click(screen.getByRole("button", { name: /entrar/i })); - - expect(screen.getByText(/campo de e-mail.*obrigatório/i)).toBeInTheDocument(); + + expect(await screen.findByText(/campo de e-mail.*obrigatório/i)).toBeInTheDocument(); expect(screen.getByText(/campo de senha.*obrigatório/i)).toBeInTheDocument(); }); - it("valida formato de email e tamanho minimo da senha", () => { - render(); - fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "qa@teste" } }); + it("valida tamanho mínimo da senha", async () => { + render(); + + fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "teste@email.com" } }); fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "123" } }); fireEvent.click(screen.getByRole("button", { name: /entrar/i })); + + expect(await screen.findByText(/pelo menos 6 caracteres/i)).toBeInTheDocument(); + }); + + it("envia formulário válido e redireciona após login", async () => { + mockLogin.mockResolvedValueOnce({ token: "fake-token-123", user: { id: 1, email: "teste@email.com" } }); + + render(); + + fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "teste@email.com" } }); + fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "123456" } }); + fireEvent.click(screen.getByRole("button", { name: /entrar/i })); + + await waitFor(() => { + expect(mockLogin).toHaveBeenCalledWith({ + email: "teste@email.com", + password: "123456", + }); + }); + + expect(localStorage.getItem("token")).toBe("fake-token-123"); + expect(window.location.href).toBe("/dashboard"); + }); - expect(screen.getByText(/insira um e-mail válido/i)).toBeInTheDocument(); - expect(screen.getByText(/pelo menos 6 caracteres/i)).toBeInTheDocument(); + it("exibe erro quando API retorna erro", async () => { + mockLogin.mockRejectedValueOnce(new Error("Credenciais inválidas")); + + render(); + + fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "teste@email.com" } }); + fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "senhaerrada" } }); + fireEvent.click(screen.getByRole("button", { name: /entrar/i })); + + expect(await screen.findByText(/Credenciais inválidas/i)).toBeInTheDocument(); }); - it("envia formulário valido", () => { + it("exibe erro genérico quando API retorna erro sem mensagem", async () => { + mockLogin.mockRejectedValueOnce(new Error("")); + render(); + + fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "teste@email.com" } }); + fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "senhaerrada" } }); + fireEvent.click(screen.getByRole("button", { name: /entrar/i })); + + expect(await screen.findByText(/Erro ao fazer login. Verifique suas credenciais./i)).toBeInTheDocument(); + }); - fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "qa@teste.com" } }); + it("mostra loading durante requisição", async () => { + mockLogin.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 200))); + + render(); + + fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "teste@email.com" } }); fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "123456" } }); fireEvent.click(screen.getByRole("button", { name: /entrar/i })); + + expect(await screen.findByRole("button", { name: /entrando\.\.\./i })).toBeDisabled(); + }); + + it("desabilita inputs durante loading", async () => { + mockLogin.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 200))); + + render(); + + const emailInput = screen.getByLabelText(/email/i); + const passwordInput = screen.getByLabelText(/senha/i); + + fireEvent.change(emailInput, { target: { value: "teste@email.com" } }); + fireEvent.change(passwordInput, { target: { value: "123456" } }); + fireEvent.click(screen.getByRole("button", { name: /entrar/i })); + + await waitFor(() => { + expect(emailInput).toBeDisabled(); + expect(passwordInput).toBeDisabled(); + }); + }); - expect(logSpy).toHaveBeenCalledWith("Formulário válido!", { - email: "qa@teste.com", - password: "123456", + it("não envia formulário quando validação falha", async () => { + render(); + + fireEvent.click(screen.getByRole("button", { name: /entrar/i })); + + await waitFor(() => { + expect(mockLogin).not.toHaveBeenCalled(); }); }); -}); + + it("checkbox de lembrete está presente", () => { + render(); + + const checkbox = screen.getByRole("checkbox"); + expect(checkbox).toBeInTheDocument(); + expect(checkbox).toBeChecked(); + }); + + it("botão de esqueceu a senha está presente", () => { + render(); + + expect(screen.getByText(/esqueceu a senha/i)).toBeInTheDocument(); + }); + +}); \ No newline at end of file diff --git a/frontend/tests/unit/services/auth.service.test.tsx b/frontend/tests/unit/services/auth.service.test.tsx new file mode 100644 index 0000000..4826905 --- /dev/null +++ b/frontend/tests/unit/services/auth.service.test.tsx @@ -0,0 +1,359 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const fetchMock = vi.fn(); +globalThis.fetch = fetchMock as any; + +import * as auth from "@/services/authService"; + +function mockResponse({ + ok = true, + status = 200, + statusText = "OK", + jsonData = {}, + textData = "", + contentType = "application/json", +}: any) { + return { + ok, + status, + statusText, + headers: { + get: (key: string) => { + if (key === "content-type") return contentType; + return null; + }, + }, + json: vi.fn().mockResolvedValue(jsonData), + text: vi.fn().mockResolvedValue(textData), + }; +} + +describe("authService", () => { + beforeEach(() => { + fetchMock.mockReset(); + vi.restoreAllMocks(); + }); + + describe("login", () => { + it("should login successfully", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + jsonData: { token: "abc", user: { id: 1, email: "test@test.com" } }, + }) + ); + + const result = await auth.login({ + email: "test@test.com", + password: "123456", + }); + + expect(fetchMock).toHaveBeenCalledWith( + expect.stringContaining("/api/auth/login"), + expect.objectContaining({ + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + }) + ); + expect(result.token).toBe("abc"); + expect(result.user.email).toBe("test@test.com"); + }); + + it("should throw error on login failure with message", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + ok: false, + status: 401, + jsonData: { message: "Invalid credentials" }, + }) + ); + + await expect( + auth.login({ email: "x@x.com", password: "wrong" }) + ).rejects.toThrow("Invalid credentials"); + }); + + it("should throw error on login failure without message", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + ok: false, + status: 500, + jsonData: {}, + }) + ); + + await expect( + auth.login({ email: "x@x.com", password: "wrong" }) + ).rejects.toThrow("Falha ao fazer login."); + }); + + it("should handle text response fallback", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + contentType: "text/plain", + textData: "Server error occurred", + }) + ); + + const result = await auth.login({ + email: "test@test.com", + password: "123456", + }); + + expect(result).toEqual({ message: "Server error occurred" }); + }); + + it("should handle empty text response", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + contentType: "text/plain", + textData: "", + }) + ); + + const result = await auth.login({ + email: "test@test.com", + password: "123456", + }); + + expect(result).toEqual({}); + }); + + it("should handle invalid JSON response", async () => { + const mockResponseObj = { + ok: true, + headers: { + get: (key: string) => { + if (key === "content-type") return "application/json"; + return null; + }, + }, + json: vi.fn().mockRejectedValue(new Error("Invalid JSON")), + text: vi.fn().mockResolvedValue(""), + }; + fetchMock.mockResolvedValueOnce(mockResponseObj as any); + + const result = await auth.login({ + email: "test@test.com", + password: "123456", + }); + + expect(result).toEqual({}); + }); + }); + + describe("register", () => { + it("should register successfully", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + jsonData: { message: "User created", user: { id: 1, email: "new@test.com" } }, + }) + ); + + const result = await auth.register({ + email: "new@test.com", + password: "123456", + name: "New User", + }); + + expect(fetchMock).toHaveBeenCalledWith( + expect.stringContaining("/api/auth/register"), + expect.objectContaining({ + method: "POST", + body: JSON.stringify({ + email: "new@test.com", + password: "123456", + name: "New User", + }), + }) + ); + expect(result.user.id).toBe(1); + }); + + it("should register with optional fields", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + jsonData: { message: "User created", user: { id: 1 } }, + }) + ); + + const result = await auth.register({ + email: "new@test.com", + password: "123456", + name: "New User", + phone: "+5511999999999", + cpf: "12345678901", + }); + + expect(result.user.id).toBe(1); + const callArgs = fetchMock.mock.calls[0][1]; + expect(JSON.parse(callArgs.body)).toEqual({ + email: "new@test.com", + password: "123456", + name: "New User", + }); + }); + + it("should throw error on register failure with message", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + ok: false, + status: 400, + jsonData: { message: "Email already exists" }, + }) + ); + + await expect( + auth.register({ + email: "existing@test.com", + password: "123456", + name: "Existing", + }) + ).rejects.toThrow("Email already exists"); + }); + + it("should throw error on register failure without message", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + ok: false, + status: 500, + jsonData: {}, + }) + ); + + await expect( + auth.register({ + email: "test@test.com", + password: "123456", + name: "Test", + }) + ).rejects.toThrow("Falha ao cadastrar: 500 OK"); + }); + + it("should handle text response on register", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + ok: true, + contentType: "text/plain", + textData: "Registration successful", + }) + ); + + const result = await auth.register({ + email: "test@test.com", + password: "123456", + name: "Test User", + }); + + expect(result).toEqual({ message: "Registration successful" }); + }); + }); + + describe("logout", () => { + it("should logout successfully", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + jsonData: {}, + }) + ); + + await expect(auth.logout()).resolves.toBeUndefined(); + expect(fetchMock).toHaveBeenCalledWith( + expect.stringContaining("/api/auth/logout"), + expect.objectContaining({ + method: "POST", + credentials: "include", + }) + ); + }); + + it("should throw on logout failure with message", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + ok: false, + status: 500, + jsonData: { message: "Session expired" }, + }) + ); + + await expect(auth.logout()).rejects.toThrow("Session expired"); + }); + + it("should throw on logout failure without message", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + ok: false, + status: 500, + jsonData: {}, + }) + ); + + await expect(auth.logout()).rejects.toThrow("Falha ao fazer logout."); + }); + }); + + describe("getCurrentUser", () => { + it("should get current user successfully", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + jsonData: { id: 1, name: "Bene", email: "bene@test.com" }, + }) + ); + + const user = await auth.getCurrentUser(); + expect(user.name).toBe("Bene"); + expect(user.email).toBe("bene@test.com"); + expect(fetchMock).toHaveBeenCalledWith( + expect.stringContaining("/api/auth/me"), + { credentials: "include" } + ); + }); + + it("should throw when getCurrentUser fails with message", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + ok: false, + status: 401, + jsonData: { message: "Not authenticated" }, + }) + ); + + await expect(auth.getCurrentUser()).rejects.toThrow("Not authenticated"); + }); + + it("should throw when getCurrentUser fails without message", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + ok: false, + status: 403, + jsonData: {}, + }) + ); + + await expect(auth.getCurrentUser()).rejects.toThrow("Falha ao carregar usuário."); + }); + }); + + describe("buildApiUrl with base URL", () => { + it("should use VITE_API_BASE_URL when configured", async () => { + const originalEnv = import.meta.env.VITE_API_BASE_URL; + import.meta.env.VITE_API_BASE_URL = "https://api.example.com"; + + fetchMock.mockResolvedValueOnce( + mockResponse({ + jsonData: { token: "abc" }, + }) + ); + + await auth.login({ email: "test@test.com", password: "123456" }); + + expect(fetchMock).toHaveBeenCalledWith( + "https://api.example.com/api/auth/login", + expect.any(Object) + ); + + import.meta.env.VITE_API_BASE_URL = originalEnv; + }); + }); +}); \ No newline at end of file diff --git a/frontend/vite.config.js b/frontend/vite.config.js index c910189..45fb667 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -4,9 +4,7 @@ import { fileURLToPath } from 'url' import { defineConfig } from 'vite' const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const apiProxyTarget = process.env.VITE_API_PROXY_TARGET || process.env.VITE_API_BASE_URL || 'http://localhost:3001' -// https://vite.dev/config/ export default defineConfig({ plugins: [react()], resolve: { @@ -15,8 +13,22 @@ export default defineConfig({ }, }, server: { + port: 5173, proxy: { - '/api': apiProxyTarget, - }, - }, -}) + '/api': { + target: 'https://jobsglobalscraper.ddns.net', + changeOrigin: true, + secure: false, + rewrite: (path) => path, + configure: (proxy, options) => { + proxy.on('error', (err, req, res) => { + console.log('proxy error', err); + }); + proxy.on('proxyReq', (proxyReq, req, res) => { + console.log('Proxying:', req.method, req.url); + }); + } + } + } + } +}) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index dffb34e..eca391e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "@commitlint/config-conventional": "^21.0.2", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^25.9.1", + "@types/node": "^25.9.2", "@types/pdfkit": "^0.17.6", "@types/pg": "^8.20.0", "@types/pino": "^7.0.5", diff --git a/package.json b/package.json index f14230e..55ef619 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "@commitlint/config-conventional": "^21.0.2", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^25.9.1", + "@types/node": "^25.9.2", "@types/pdfkit": "^0.17.6", "@types/pg": "^8.20.0", "@types/pino": "^7.0.5", From 7d6816be297e1e7fb533dc682f4a1ef39b72e333 Mon Sep 17 00:00:00 2001 From: Benevanio Santos Date: Tue, 9 Jun 2026 23:03:34 -0300 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20PAV-51=20corre=C3=A7=C3=A3o=20do=20v?= =?UTF-8?q?ercel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/vite.config.js | 39 +++++---------------------------------- 1 file changed, 5 insertions(+), 34 deletions(-) diff --git a/frontend/vite.config.js b/frontend/vite.config.js index f1434ce..c4a9395 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -5,6 +5,8 @@ import { defineConfig } from 'vite' const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const isDev = process.env.NODE_ENV === 'development' + export default defineConfig({ plugins: [react()], resolve: { @@ -12,45 +14,14 @@ export default defineConfig({ '@': path.resolve(__dirname, './src'), }, }, - server: { + server: isDev ? { port: 5173, proxy: { '/api': { target: 'https://jobsglobalscraper.ddns.net', changeOrigin: true, secure: false, - rewrite: (path) => path, - configure: (proxy, options) => { - proxy.on('error', (err, req, res) => { - console.log('proxy error', err); - }); - proxy.on('proxyReq', (proxyReq, req, res) => { - console.log('Proxying:', req.method, req.url); - }); - } } } - } -}) - target: apiProxyTarget, - changeOrigin: true, - secure: false, - }, - '/auth': { - target: apiProxyTarget, - changeOrigin: true, - secure: false, - configure: (proxy) => { - proxy.on('error', (err) => { - console.error('Erro no proxy do Vite para /auth:', err.message); - }); - }, - }, - }, - }, - test: { - globals: true, - environment: 'jsdom', - setupFiles: ['./tests/setup.js'], - }, -}) + } : undefined, +}) \ No newline at end of file From 29e9768c9a2dda529c8cd705cf1e12e20b1f7bd6 Mon Sep 17 00:00:00 2001 From: Benevanio Santos Date: Tue, 9 Jun 2026 23:14:13 -0300 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20PAV-51=20=20corre=C3=A7=C3=A3o=20do?= =?UTF-8?q?=20arquivios=20com=20=20conflito?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/login/RegisterSide.tsx | 257 ++---------------- frontend/src/components/login/RigthSide.tsx | 208 +++----------- 2 files changed, 57 insertions(+), 408 deletions(-) diff --git a/frontend/src/components/login/RegisterSide.tsx b/frontend/src/components/login/RegisterSide.tsx index 60fc92b..aa99eef 100644 --- a/frontend/src/components/login/RegisterSide.tsx +++ b/frontend/src/components/login/RegisterSide.tsx @@ -1,20 +1,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { register } from "@/services/authService"; // ← MUDE para register -import { useState, FormEvent } from "react"; -import { Eye, EyeOff, ArrowLeft, Github, Linkedin } from "lucide-react"; +import { register } from "@/services/authService"; import { Image } from "@unpic/react"; import { motion } from "framer-motion"; import { ArrowLeft, Eye, EyeOff } from "lucide-react"; import { FormEvent, useState } from "react"; import PhoneInput from "react-phone-number-input"; - -import { api } from "@/services/api"; import "react-phone-number-input/style.css"; -import { useNavigate } from "react-router-dom"; - -interface ValidationError { - _errors?: string[]; -} const STATIC_STARS = Array.from({ length: 40 }).map((_, i) => { const random = (min: number, max: number) => Math.random() * (max - min) + min; @@ -58,8 +49,6 @@ function StarsBackground() { } export default function RegisterSide() { - const navigate = useNavigate(); - const [showPassword, setShowPassword] = useState(false); const [nome, setNome] = useState(""); const [email, setEmail] = useState(""); @@ -75,8 +64,6 @@ export default function RegisterSide() { const [isLoading, setIsLoading] = useState(false); const [apiError, setApiError] = useState(""); - const [globalError, setGlobalError] = useState(""); - const [isSubmitting, setIsSubmitting] = useState(false); const handleRevealPassword = () => setShowPassword((prev) => !prev); @@ -88,36 +75,6 @@ export default function RegisterSide() { return `${digits.slice(0, 3)}.${digits.slice(3, 6)}.${digits.slice(6, 9)}-${digits.slice(9)}`; }; - const handleOAuthLogin = async (provider: "google" | "github" | "linkedin") => { - try { - setGlobalError(""); - setIsSubmitting(true); - - const response = await api.get(`/auth/${provider}/url`); - - if (response.data?.url) { - window.location.href = response.data.url; - } else { - throw new Error("URL de redirecionamento não retornada pelo servidor."); - } - } catch (error: unknown) { - console.error("Erro ao iniciar fluxo OAuth:", error); - - let apiError = `Não foi possível conectar ao provedor ${provider}.`; - - // Verifica de forma segura se o erro contém um objeto response vindo do Axios - if (error && typeof error === "object" && "response" in error) { - const axiosError = error as { response?: { data?: { error?: string } } }; - if (typeof axiosError.response?.data?.error === "string") { - apiError = axiosError.response.data.error; - } - } - - setGlobalError(apiError); - setIsSubmitting(false); - } - }; - const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setNomeError(""); @@ -126,7 +83,6 @@ export default function RegisterSide() { setPasswordError(""); setCpfError(""); setApiError(""); - setGlobalError(""); let isValid = true; @@ -167,91 +123,34 @@ export default function RegisterSide() { if (isValid) { setIsLoading(true); try { - const result = await register({ + await register({ email: email, password: password, name: nome, }); - console.log("Cadastro bem sucedido:", result); window.location.href = "/login?registered=true"; } catch (error: any) { console.error("Erro no cadastro:", error); setApiError(error.message || "Erro ao cadastrar. Tente novamente."); } finally { setIsLoading(false); - if (!isValid) return; - - setIsSubmitting(true); - - try { - const response = await api.post("/auth/register", { - name: nome, - email: email, - phone: telefone, - password: password, - cpf: cpf.replace(/\D/g, ""), - }); - - console.log("Cadastro efetuado com sucesso!", response.data); - navigate("/app"); - - } catch (error: unknown) { - console.error("Erro na requisição de cadastro:", error); - setIsSubmitting(false); - - if (error && typeof error === "object" && "response" in error) { - const axiosError = error as { - response?: { - data?: { - error?: string | Record; - message?: string; - }; - }; - }; - - const data = axiosError.response?.data; - - if (data && typeof data.error === "object") { - const validationErrors = data.error as Record; - - if (validationErrors.name?._errors?.[0]) setNomeError(validationErrors.name._errors[0]); - if (validationErrors.email?._errors?.[0]) setEmailError(validationErrors.email._errors[0]); - if (validationErrors.phone?._errors?.[0]) setTelefoneError(validationErrors.phone._errors[0]); - if (validationErrors.password?._errors?.[0]) setPasswordError(validationErrors.password._errors[0]); - if (validationErrors.cpf?._errors?.[0]) setCpfError(validationErrors.cpf._errors[0]); - } else { - const fallbackMsg = data?.message || (typeof data?.error === "string" ? data.error : "") || "Erro ao efetuar o cadastro."; - if (fallbackMsg.toLowerCase().includes("email")) { - setEmailError("Este endereço de e-mail já está em uso."); - } else { - setGlobalError(fallbackMsg); - } - } - } else { - setGlobalError("Ocorreu um erro inesperado de conexão."); } } }; return (
-
-
-

< @@ -266,143 +165,48 @@ export default function RegisterSide() {

-
- {apiError && (

{apiError}

- {globalError && ( -
- {globalError}
)} -
- - setNome(e.target.value)} - placeholder="benevanio" - disabled={isLoading} - className={`w-full px-4 py-3.5 rounded-xl border bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 transition-all shadow-sm ${ - nomeError ? "border-red-500 focus:ring-red-500" : "border-gray-200 dark:border-neutral-700 focus:ring-blue-500 focus:border-transparent" - } ${isLoading ? "opacity-50 cursor-not-allowed" : ""}`} - /> + + setNome(e.target.value)} placeholder="benevanio" disabled={isLoading} className={`w-full px-4 py-3.5 rounded-xl border bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 transition-all shadow-sm ${nomeError ? "border-red-500 focus:ring-red-500" : "border-gray-200 dark:border-neutral-700 focus:ring-blue-500 focus:border-transparent"} ${isLoading ? "opacity-50 cursor-not-allowed" : ""}`} /> {nomeError &&

{nomeError}

}
-
- - setEmail(e.target.value)} - placeholder="benevanio@dev.com.br" - disabled={isLoading} - className={`w-full px-4 py-3.5 rounded-xl border bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 transition-all shadow-sm ${ - emailError ? "border-red-500 focus:ring-red-500" : "border-gray-200 dark:border-neutral-700 focus:ring-blue-500 focus:border-transparent" - } ${isLoading ? "opacity-50 cursor-not-allowed" : ""}`} - /> + + setEmail(e.target.value)} placeholder="benevanio@dev.com.br" disabled={isLoading} className={`w-full px-4 py-3.5 rounded-xl border bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 transition-all shadow-sm ${emailError ? "border-red-500 focus:ring-red-500" : "border-gray-200 dark:border-neutral-700 focus:ring-blue-500 focus:border-transparent"} ${isLoading ? "opacity-50 cursor-not-allowed" : ""}`} /> {emailError &&

{emailError}

}
-
- -
- + +
+
{telefoneError &&

{telefoneError}

}
-
- +
- setPassword(e.target.value)} - placeholder="Ex: ••••••••••••" - disabled={isLoading} - className={`w-full px-4 py-3.5 rounded-xl border bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 transition-all shadow-sm ${ - passwordError ? "border-red-500 focus:ring-red-500" : "border-gray-200 dark:border-neutral-700 focus:ring-blue-500 focus:border-transparent" - } ${isLoading ? "opacity-50 cursor-not-allowed" : ""}`} - /> -
{passwordError &&

{passwordError}

}
-
- - setCpf(formatCpf(e.target.value))} - placeholder="091.000.000-00" - disabled={isLoading} - className={`w-full px-4 py-3.5 rounded-xl border bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 transition-all shadow-sm ${ - cpfError ? "border-red-500 focus:ring-red-500" : "border-gray-200 dark:border-neutral-700 focus:ring-blue-500 focus:border-transparent" - } ${isLoading ? "opacity-50 cursor-not-allowed" : ""}`} - /> + + setCpf(formatCpf(e.target.value))} placeholder="091.000.000-00" disabled={isLoading} className={`w-full px-4 py-3.5 rounded-xl border bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 transition-all shadow-sm ${cpfError ? "border-red-500 focus:ring-red-500" : "border-gray-200 dark:border-neutral-700 focus:ring-blue-500 focus:border-transparent"} ${isLoading ? "opacity-50 cursor-not-allowed" : ""}`} /> {cpfError &&

{cpfError}

}
- - + {isLoading ? "Cadastrando..." : "Cadastrar"} - disabled={isSubmitting} - whileHover={{ scale: isSubmitting ? 1 : 1.01 }} - whileTap={{ scale: isSubmitting ? 1 : 0.99 }} - className="w-full bg-gradient-to-r from-blue-600 via-purple-600 to-teal-600 hover:opacity-95 text-white py-3.5 px-4 rounded-xl font-bold text-base focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all shadow-md shadow-blue-500/10 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed" - > - {isSubmitting ? "Cadastrando..." : "Cadastrar"} -
@@ -412,7 +216,6 @@ export default function RegisterSide() { Ou cadastre-se com
-
- - - -
-
); } \ No newline at end of file diff --git a/frontend/src/components/login/RigthSide.tsx b/frontend/src/components/login/RigthSide.tsx index df218e1..a7e06e3 100644 --- a/frontend/src/components/login/RigthSide.tsx +++ b/frontend/src/components/login/RigthSide.tsx @@ -1,16 +1,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { login } from "@/services/authService" -import { Image } from "@unpic/react" -import { motion } from "framer-motion" -import { ArrowLeft, Eye, EyeOff } from "lucide-react" -import { FormEvent, useState } from "react" -import { useState, FormEvent } from "react" -import { Eye, EyeOff, ArrowLeft, Github, Linkedin } from "lucide-react" -import { Image } from "@unpic/react" -import { motion } from "framer-motion" -import { useNavigate } from "react-router-dom" -import { useAuth } from "@/context/AuthContext" -import { api } from "@/services/api" +import { login } from "@/services/authService"; +import { Image } from "@unpic/react"; +import { motion } from "framer-motion"; +import { ArrowLeft, Eye, EyeOff } from "lucide-react"; +import { FormEvent, useState } from "react"; const STATIC_STARS = Array.from({ length: 40 }).map((_, i) => { const random = (min: number, max: number) => Math.random() * (max - min) + min; @@ -54,142 +47,75 @@ function StarsBackground() { } export default function RightSide() { - const navigate = useNavigate(); - const { login } = useAuth(); + const [showPassword, setShowPassword] = useState(false); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [emailError, setEmailError] = useState(""); + const [passwordError, setPasswordError] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [apiError, setApiError] = useState(""); - const [showPassword, setShowPassword] = useState(false) - const [email, setEmail] = useState("") - const [password, setPassword] = useState("") - const [emailError, setEmailError] = useState("") - const [passwordError, setPasswordError] = useState("") - const [isLoading, setIsLoading] = useState(false) - const [apiError, setApiError] = useState("") - - const handleRevealPassword = () => setShowPassword((prev) => !prev) - - const [globalError, setGlobalError] = useState("") - const [isSubmitting, setIsSubmitting] = useState(false) - - const handleRevealPassword = () => setShowPassword((prev) => !prev) - - async function handleOAuthLogin(provider: "google" | "github" | "linkedin") { - try { - setGlobalError(""); - setIsSubmitting(true); - - const response = await api.get(`/auth/${provider}/url`); - - if (response.data?.url) { - window.location.href = response.data.url; - } else { - throw new Error("URL de redirecionamento não encontrada."); - } - } catch (error: unknown) { - console.error(`Erro ao iniciar OAuth com ${provider}:`, error); - - let apiError = `Não foi possível conectar ao provedor de login com o ${provider}.`; - - // Validação defensiva do erro do Axios - if (error && typeof error === "object" && "response" in error) { - const axiosError = error as { response?: { data?: { error?: string } } }; - if (typeof axiosError.response?.data?.error === "string") { - apiError = axiosError.response.data.error; - } - } - - setGlobalError(apiError); - setIsSubmitting(false); - } - } + const handleRevealPassword = () => setShowPassword((prev) => !prev); const handleSubmit = async (e: FormEvent) => { - e.preventDefault() + e.preventDefault(); - setEmailError("") - setPasswordError("") - setApiError("") - setGlobalError("") + setEmailError(""); + setPasswordError(""); + setApiError(""); - let isValid = true + let isValid = true; if (!email) { - setEmailError("O campo de e-mail é obrigatório.") - isValid = false + setEmailError("O campo de e-mail é obrigatório."); + isValid = false; } else { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { - setEmailError("Por favor, insira um e-mail válido.") - isValid = false + setEmailError("Por favor, insira um e-mail válido."); + isValid = false; } } if (!password) { - setPasswordError("O campo de senha é obrigatório.") - isValid = false + setPasswordError("O campo de senha é obrigatório."); + isValid = false; } else if (password.length < 6) { - setPasswordError("A senha precisa conter pelo menos 6 caracteres.") - isValid = false + setPasswordError("A senha precisa conter pelo menos 6 caracteres."); + isValid = false; } if (isValid) { - setIsLoading(true) + setIsLoading(true); try { - const result = await login({ email, password }) + const result = await login({ email, password }); if (result.token) { - localStorage.setItem("token", result.token) + localStorage.setItem("token", result.token); } - window.location.href = "/dashboard" + window.location.href = "/dashboard"; } catch (error: any) { - setApiError(error.message || "Erro ao fazer login. Verifique suas credenciais.") + setApiError(error.message || "Erro ao fazer login. Verifique suas credenciais."); } finally { - setIsLoading(false) + setIsLoading(false); } - if (!isValid) return; - - setIsSubmitting(true) - - try { - await login({ email, password }); - navigate("/app"); - } catch (err: unknown) { - console.error(err); - let errorMessage = "Credenciais inválidas ou erro de conexão."; - - // Validação defensiva do erro repassado pelo Contexto/Axios - if (err && typeof err === "object" && "response" in err) { - const axiosError = err as { response?: { data?: { error?: string } } }; - if (typeof axiosError.response?.data?.error === "string") { - errorMessage = axiosError.response.data.error; - } - } - - setGlobalError(errorMessage); - } finally { - setIsSubmitting(false) } - } + }; return (
-
-
-

< @@ -204,19 +130,12 @@ export default function RightSide() {

-
- {apiError && (

{apiError}

- - {globalError && ( -
- {globalError}
)} -
-
{passwordError &&

{passwordError}

}
-
- {isLoading ? "Entrando..." : "Entrar"} - disabled={isSubmitting} - whileHover={{ scale: isSubmitting ? 1 : 1.01 }} - whileTap={{ scale: isSubmitting ? 1 : 0.99 }} - className="w-full bg-gradient-to-r from-blue-600 via-purple-600 to-teal-600 hover:opacity-95 text-white py-3.5 px-4 rounded-xl font-bold text-base focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all shadow-md shadow-blue-500/10 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed" - > - {isSubmitting ? "Entrando..." : "Entrar"}
-
@@ -310,55 +217,20 @@ export default function RightSide() { Ou faça login com
-
- - - - -
-
- ) -} + ); +} \ No newline at end of file