Skip to content

Commit 99a7dfd

Browse files
Jeremias Santosclaude
authored andcommitted
feat(pav-58): integrar autenticacao GitHub OAuth no frontend
- Adicionar funcao getGithubAuthUrl no authService - Substituir botao Apple por GitHub (SVG + handler) nas telas de login e registro - Adicionar PublicRoute para redirecionar usuarios logados de /login e /register para /app - Adicionar testes unitarios para getGithubAuthUrl Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1cec022 commit 99a7dfd

5 files changed

Lines changed: 95 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: 13 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 } from "@/services/authService";
2+
import { register, getGoogleAuthUrl, getGithubAuthUrl } 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,16 @@ export default function RegisterSide() {
7575
}
7676
};
7777

78+
const handleGithubLogin = async () => {
79+
setIsLoading(true);
80+
try {
81+
const url = await getGithubAuthUrl();
82+
window.location.href = url;
83+
} catch {
84+
setIsLoading(false);
85+
}
86+
};
87+
7888
const handleRevealPassword = () => setShowPassword((prev) => !prev);
7989

8090
const formatCpf = (value: string) => {
@@ -233,9 +243,9 @@ export default function RegisterSide() {
233243
<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">
234244
<Image src="/facebook.png" alt="Facebook" width={20} height={20} className="object-contain" />
235245
</button>
236-
<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">
246+
<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">
237247
<svg className="h-5 w-5 fill-gray-900 dark:fill-white transition-colors" viewBox="0 0 24 24">
238-
<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" />
248+
<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" />
239249
</svg>
240250
</button>
241251
</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 } from "@/services/authService";
2+
import { login, getGoogleAuthUrl, getGithubAuthUrl } 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 handleRevealPassword = () => setShowPassword((prev) => !prev);
7183

7284
const handleSubmit = async (e: FormEvent) => {
@@ -236,9 +248,9 @@ export default function RightSide() {
236248
<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">
237249
<Image src="/facebook.png" alt="Facebook" width={20} height={20} className="object-contain" />
238250
</button>
239-
<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">
251+
<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">
240252
<svg className="h-5 w-5 fill-gray-900 dark:fill-white transition-colors" viewBox="0 0 24 24">
241-
<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" />
253+
<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" />
242254
</svg>
243255
</button>
244256
</div>

frontend/src/services/authService.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,5 +135,19 @@ export async function getGoogleAuthUrl(): Promise<string> {
135135
throw createError(payload, "Falha ao obter URL de autenticacao Google.");
136136
}
137137

138+
return payload.url;
139+
}
140+
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+
138152
return payload.url;
139153
}

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)