Skip to content

Commit 8733ae0

Browse files
authored
Develop (#150)
2 parents a38e519 + c131cdd commit 8733ae0

7 files changed

Lines changed: 171 additions & 8 deletions

File tree

frontend/src/App.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) {
1616
return <>{children}</>;
1717
}
1818

19+
function PublicRoute({ children }: { children: React.ReactNode }) {
20+
const { user, isLoading } = useAuth();
21+
if (isLoading) return <Loading />;
22+
if (user) return <Navigate to="/app" replace />;
23+
return <>{children}</>;
24+
}
25+
1926
function App() {
2027
const [appCarregando, setAppCarregando] = useState(true);
2128

@@ -42,8 +49,8 @@ function App() {
4249
</ProtectedRoute>
4350
}
4451
/>
45-
<Route path="/login" element={<LoginPage />} />
46-
<Route path="/register" element={<RegisterPage />} />
52+
<Route path="/login" element={<PublicRoute><LoginPage /></PublicRoute>} />
53+
<Route path="/register" element={<PublicRoute><RegisterPage /></PublicRoute>} />
4754
<Route path="/auth/callback" element={<AuthCallback />} />
4855
<Route path="*" element={<NotFound />} />
4956
</Routes>

frontend/src/components/login/RegisterSide.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
import { register, getGoogleAuthUrl, getLinkedinAuthUrl } from "@/services/authService";
2+
import { register, getGoogleAuthUrl, getGithubAuthUrl, getLinkedinAuthUrl } from "@/services/authService";
33
import { Image } from "@unpic/react";
44
import { motion } from "framer-motion";
55
import { ArrowLeft, Eye, EyeOff } from "lucide-react";
@@ -75,6 +75,18 @@ export default function RegisterSide() {
7575
}
7676
};
7777

78+
const handleGithubLogin = async () => {
79+
setIsLoading(true);
80+
setApiError("");
81+
try {
82+
const url = await getGithubAuthUrl();
83+
window.location.href = url;
84+
} catch (err: any) {
85+
setApiError(err.message || "Erro ao iniciar login com Github.");
86+
setIsLoading(false);
87+
}
88+
};
89+
7890
const handleLinkedinLogin = async () => {
7991
setIsLoading(true);
8092
setApiError("");
@@ -247,9 +259,9 @@ export default function RegisterSide() {
247259
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
248260
</svg>
249261
</button>
250-
<button type="button" disabled={isLoading} className="flex justify-center items-center py-3 px-4 border border-gray-200 dark:border-neutral-800 rounded-xl bg-white/50 dark:bg-neutral-800/50 hover:bg-gray-50 dark:hover:bg-neutral-700/50 transition-all shadow-sm cursor-pointer disabled:opacity-50">
262+
<button type="button" onClick={handleGithubLogin} disabled={isLoading} className="flex justify-center items-center py-3 px-4 border border-gray-200 dark:border-neutral-800 rounded-xl bg-white/50 dark:bg-neutral-800/50 hover:bg-gray-50 dark:hover:bg-neutral-700/50 transition-all shadow-sm cursor-pointer disabled:opacity-50">
251263
<svg className="h-5 w-5 fill-gray-900 dark:fill-white transition-colors" viewBox="0 0 24 24">
252-
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M15.97 4.17c.66-.81 1.11-1.93.99-3.06-1 .04-2.21.67-2.93 1.49-.62.69-1.16 1.84-1.01 2.96 1.12.09 2.27-.58 2.95-1.39z" />
264+
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
253265
</svg>
254266
</button>
255267
</div>

frontend/src/components/login/RigthSide.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
import { login, getGoogleAuthUrl, getLinkedinAuthUrl } from "@/services/authService";
2+
import { login, getGoogleAuthUrl, getGithubAuthUrl, getLinkedinAuthUrl } from "@/services/authService";
33
import { Image } from "@unpic/react";
44
import { motion } from "framer-motion";
55
import { ArrowLeft, Eye, EyeOff } from "lucide-react";
@@ -67,6 +67,18 @@ export default function RightSide() {
6767
}
6868
};
6969

70+
const handleGithubLogin = async () => {
71+
setIsLoading(true);
72+
setApiError("");
73+
try {
74+
const url = await getGithubAuthUrl();
75+
window.location.href = url;
76+
} catch (err: any) {
77+
setApiError(err.message || "Erro ao iniciar login com Github.");
78+
setIsLoading(false);
79+
}
80+
};
81+
7082
const handleLinkedinLogin = async () => {
7183
setIsLoading(true);
7284
setApiError("");
@@ -250,9 +262,9 @@ export default function RightSide() {
250262
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
251263
</svg>
252264
</button>
253-
<button type="button" disabled={isLoading} className="flex justify-center items-center py-3 px-4 border border-gray-200 dark:border-neutral-800 rounded-xl bg-white/50 dark:bg-neutral-800/50 hover:bg-gray-50 dark:hover:bg-neutral-700/50 transition-all shadow-sm cursor-pointer disabled:opacity-50">
265+
<button type="button" onClick={handleGithubLogin} disabled={isLoading} className="flex justify-center items-center py-3 px-4 border border-gray-200 dark:border-neutral-800 rounded-xl bg-white/50 dark:bg-neutral-800/50 hover:bg-gray-50 dark:hover:bg-neutral-700/50 transition-all shadow-sm cursor-pointer disabled:opacity-50">
254266
<svg className="h-5 w-5 fill-gray-900 dark:fill-white transition-colors" viewBox="0 0 24 24">
255-
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M15.97 4.17c.66-.81 1.11-1.93.99-3.06-1 .04-2.21.67-2.93 1.49-.62.69-1.16 1.84-1.01 2.96 1.12.09 2.27-.58 2.95-1.39z" />
267+
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
256268
</svg>
257269
</button>
258270
</div>

frontend/src/services/authService.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,20 @@ export async function getGoogleAuthUrl(): Promise<string> {
138138
return payload.url;
139139
}
140140

141+
export async function getGithubAuthUrl(): Promise<string> {
142+
const response = await fetch(buildUrl("/api/auth/github/url"), {
143+
credentials: "include",
144+
});
145+
146+
const payload = await parseResponse(response);
147+
148+
if (!response.ok) {
149+
throw createError(payload, "Falha ao obter URL de autenticacao Github.");
150+
}
151+
152+
return payload.url;
153+
}
154+
141155
export async function getLinkedinAuthUrl(): Promise<string> {
142156
const response = await fetch(buildUrl("/api/auth/linkedin/url"), {
143157
credentials: "include",

frontend/tests/unit/components/login/RegisterSide.test.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
33
import { beforeEach, describe, expect, it, vi } from "vitest";
44

55
const mockRegister = vi.fn();
6+
const mockGetGoogleAuthUrl = vi.fn();
7+
const mockGetGithubAuthUrl = vi.fn();
68
const mockGetLinkedinAuthUrl = vi.fn();
79

810
vi.mock("@/services/authService", () => ({
911
register: (...args: any[]) => mockRegister(...args),
12+
getGoogleAuthUrl: (...args: any[]) => mockGetGoogleAuthUrl(...args),
13+
getGithubAuthUrl: (...args: any[]) => mockGetGithubAuthUrl(...args),
1014
getLinkedinAuthUrl: (...args: any[]) => mockGetLinkedinAuthUrl(...args),
1115
}));
1216

@@ -40,6 +44,8 @@ describe("RegisterSide", () => {
4044

4145
beforeEach(() => {
4246
mockRegister.mockReset();
47+
mockGetGoogleAuthUrl.mockReset();
48+
mockGetGithubAuthUrl.mockReset();
4349
mockGetLinkedinAuthUrl.mockReset();
4450
Object.defineProperty(window, "location", {
4551
configurable: true,
@@ -153,6 +159,37 @@ describe("RegisterSide", () => {
153159
});
154160
});
155161

162+
it("redireciona para GitHub OAuth ao clicar no botao GitHub", async () => {
163+
mockGetGithubAuthUrl.mockResolvedValueOnce(
164+
"https://github.com/login/oauth/authorize?state=abc"
165+
);
166+
167+
render(<RegisterSide />);
168+
169+
const buttons = screen.getAllByRole("button");
170+
const githubButton = buttons.find(btn => btn.querySelector('svg.fill-gray-900'));
171+
fireEvent.click(githubButton!);
172+
173+
await waitFor(() => {
174+
expect(mockGetGithubAuthUrl).toHaveBeenCalled();
175+
expect(window.location.href).toBe(
176+
"https://github.com/login/oauth/authorize?state=abc"
177+
);
178+
});
179+
});
180+
181+
it("exibe erro quando GitHub OAuth falha", async () => {
182+
mockGetGithubAuthUrl.mockRejectedValueOnce(new Error("Github indisponível"));
183+
184+
render(<RegisterSide />);
185+
186+
const buttons = screen.getAllByRole("button");
187+
const githubButton = buttons.find(btn => btn.querySelector('svg.fill-gray-900'));
188+
fireEvent.click(githubButton!);
189+
190+
expect(await screen.findByText(/Github indisponível/i)).toBeInTheDocument();
191+
});
192+
156193
it("redireciona para LinkedIn OAuth ao clicar no botao LinkedIn", async () => {
157194
mockGetLinkedinAuthUrl.mockResolvedValueOnce(
158195
"https://www.linkedin.com/oauth/v2/authorization?state=abc"

frontend/tests/unit/components/login/RigthSide.test.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
44
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
55

66
const mockLogin = vi.fn();
7+
const mockGetGoogleAuthUrl = vi.fn();
8+
const mockGetGithubAuthUrl = vi.fn();
79
const mockGetLinkedinAuthUrl = vi.fn();
810

911
vi.mock("@/services/authService", () => ({
1012
login: (...args: any[]) => mockLogin(...args),
13+
getGoogleAuthUrl: (...args: any[]) => mockGetGoogleAuthUrl(...args),
14+
getGithubAuthUrl: (...args: any[]) => mockGetGithubAuthUrl(...args),
1115
getLinkedinAuthUrl: (...args: any[]) => mockGetLinkedinAuthUrl(...args),
1216
}));
1317

@@ -33,6 +37,8 @@ describe("RigthSide", () => {
3337

3438
beforeEach(() => {
3539
mockLogin.mockReset();
40+
mockGetGoogleAuthUrl.mockReset();
41+
mockGetGithubAuthUrl.mockReset();
3642
mockGetLinkedinAuthUrl.mockReset();
3743
Object.defineProperty(window, "location", {
3844
configurable: true,
@@ -161,6 +167,37 @@ describe("RigthSide", () => {
161167
expect(screen.getByText(/esqueceu a senha/i)).toBeInTheDocument();
162168
});
163169

170+
it("redireciona para GitHub OAuth ao clicar no botao GitHub", async () => {
171+
mockGetGithubAuthUrl.mockResolvedValueOnce(
172+
"https://github.com/login/oauth/authorize?state=abc"
173+
);
174+
175+
render(<RigthSide />);
176+
177+
const buttons = screen.getAllByRole("button");
178+
const githubButton = buttons.find(btn => btn.querySelector('svg.fill-gray-900'));
179+
fireEvent.click(githubButton!);
180+
181+
await waitFor(() => {
182+
expect(mockGetGithubAuthUrl).toHaveBeenCalled();
183+
expect(window.location.href).toBe(
184+
"https://github.com/login/oauth/authorize?state=abc"
185+
);
186+
});
187+
});
188+
189+
it("exibe erro quando GitHub OAuth falha", async () => {
190+
mockGetGithubAuthUrl.mockRejectedValueOnce(new Error("Github indisponível"));
191+
192+
render(<RigthSide />);
193+
194+
const buttons = screen.getAllByRole("button");
195+
const githubButton = buttons.find(btn => btn.querySelector('svg.fill-gray-900'));
196+
fireEvent.click(githubButton!);
197+
198+
expect(await screen.findByText(/Github indisponível/i)).toBeInTheDocument();
199+
});
200+
164201
it("redireciona para LinkedIn OAuth ao clicar no botao LinkedIn", async () => {
165202
mockGetLinkedinAuthUrl.mockResolvedValueOnce(
166203
"https://www.linkedin.com/oauth/v2/authorization?state=abc"

frontend/tests/unit/services/auth.service.test.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,50 @@ describe("authService", () => {
253253
});
254254
});
255255

256+
describe("getGithubAuthUrl", () => {
257+
it("should return Github auth URL on success", async () => {
258+
fetchMock.mockResolvedValueOnce(
259+
mockResponse({
260+
jsonData: { url: "https://github.com/login/oauth/authorize?state=abc" },
261+
})
262+
);
263+
264+
const url = await auth.getGithubAuthUrl();
265+
266+
expect(url).toBe("https://github.com/login/oauth/authorize?state=abc");
267+
expect(fetchMock).toHaveBeenCalledWith(
268+
expect.stringContaining("/api/auth/github/url"),
269+
expect.objectContaining({ credentials: "include" })
270+
);
271+
});
272+
273+
it("should throw error on failure with message", async () => {
274+
fetchMock.mockResolvedValueOnce(
275+
mockResponse({
276+
ok: false,
277+
status: 500,
278+
jsonData: { message: "Provider unavailable" },
279+
})
280+
);
281+
282+
await expect(auth.getGithubAuthUrl()).rejects.toThrow("Provider unavailable");
283+
});
284+
285+
it("should throw error on failure without message", async () => {
286+
fetchMock.mockResolvedValueOnce(
287+
mockResponse({
288+
ok: false,
289+
status: 500,
290+
jsonData: {},
291+
})
292+
);
293+
294+
await expect(auth.getGithubAuthUrl()).rejects.toThrow(
295+
"Falha ao obter URL de autenticacao Github."
296+
);
297+
});
298+
});
299+
256300
describe("getGoogleAuthUrl", () => {
257301
it("should return Google auth URL on success", async () => {
258302
fetchMock.mockResolvedValueOnce(

0 commit comments

Comments
 (0)