diff --git a/package-lock.json b/package-lock.json index f03c3b0..b3632ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@testing-library/react": "^16.3.1", "@testing-library/user-event": "^13.5.0", "lucide-react": "^0.562.0", + "oidc-client-ts": "^3.1.0", "react": "^19.2.3", "react-dom": "^19.2.3", "react-scripts": "5.0.1", @@ -10949,6 +10950,15 @@ "node": ">=4.0" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -11757,6 +11767,18 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "license": "MIT" }, + "node_modules/oidc-client-ts": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.4.1.tgz", + "integrity": "sha512-jNdst/U28Iasukx/L5MP6b274Vr7ftQs6qAhPBCvz6Wt5rPCA+Q/tUmCzfCHHWweWw5szeMy2Gfrm1rITwUKrw==", + "license": "Apache-2.0", + "dependencies": { + "jwt-decode": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", diff --git a/package.json b/package.json index 08b9dd4..faf05c3 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.1", "@testing-library/user-event": "^13.5.0", + "oidc-client-ts": "^3.1.0", "lucide-react": "^0.562.0", "react": "^19.2.3", "react-dom": "^19.2.3", diff --git a/src/App.js b/src/App.js index 36d0bd5..11fb018 100644 --- a/src/App.js +++ b/src/App.js @@ -1,6 +1,8 @@ import React, { useState, useEffect, useRef } from 'react'; import { Database, Globe, Download, Play, Pause, Trash2, Plus, RefreshCw, Activity, Clock, CircleX, CircleCheck, Grid, Calendar, Terminal } from 'lucide-react'; import * as signalR from '@microsoft/signalr'; +import { authFetch } from './auth/authFetch'; +import { getAccessToken } from './auth/userManager'; const API_BASE = 'http://localhost:5123'; @@ -36,7 +38,9 @@ const WebScraper = () => { // Conectar ao SignalR useEffect(() => { const connection = new signalR.HubConnectionBuilder() - .withUrl(`http://localhost:5123/ws/scrape`) + .withUrl(`${API_BASE}/ws/scrape`, { + accessTokenFactory: async () => (await getAccessToken()) ?? '' + }) .withAutomaticReconnect() .build(); @@ -208,7 +212,7 @@ const WebScraper = () => { const loadSessions = async () => { try { - const response = await fetch(`${API_BASE}/sessions`); + const response = await authFetch(`${API_BASE}/sessions`); const data = await response.json(); setSessions(data); } catch (err) { @@ -218,7 +222,7 @@ const WebScraper = () => { const loadSessionStatus = async (sessionId) => { try { - const response = await fetch(`${API_BASE}/sessions/${sessionId}/scrap/status`); + const response = await authFetch(`${API_BASE}/sessions/${sessionId}/scrap/status`); const data = await response.json(); setSessionStatus(data); } catch (err) { @@ -228,7 +232,7 @@ const WebScraper = () => { const startScrapingById = async (sessionId, sessionName) => { try { - await fetch(`${API_BASE}/sessions/${sessionId}/scrap/start`, { + await authFetch(`${API_BASE}/sessions/${sessionId}/scrap/start`, { method: 'POST' }); addLog(`Iniciando scraping da sessão: ${sessionName}`, 'info'); @@ -239,7 +243,7 @@ const WebScraper = () => { const stopScrapingById = async (sessionId, sessionName) => { try { - await fetch(`${API_BASE}/sessions/${sessionId}/scrap/stop`, { + await authFetch(`${API_BASE}/sessions/${sessionId}/scrap/stop`, { method: 'POST' }); addLog(`Parando sessão: ${sessionName}`, 'warning'); @@ -262,7 +266,7 @@ const WebScraper = () => { if (!selectedSession || !newUrl) return; try { - const response = await fetch(`${API_BASE}/sessions/${selectedSession.sessionId}/queue`, { + const response = await authFetch(`${API_BASE}/sessions/${selectedSession.sessionId}/queue`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: newUrl }) diff --git a/src/AuthBootstrap.js b/src/AuthBootstrap.js new file mode 100644 index 0000000..edf898b --- /dev/null +++ b/src/AuthBootstrap.js @@ -0,0 +1,82 @@ +import React, { useEffect, useState } from "react"; +import { userManager } from "./auth/userManager"; + +/** + * Responsável por: + * - tratar /callback (retorno do Authentik) + * - garantir que o usuário esteja logado antes de renderizar o app + */ +export default function AuthBootstrap({ children }) { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + (async () => { + try { + const { pathname, search } = window.location; + + if (pathname === "/callback") { + // Finaliza o fluxo OIDC + await userManager.signinRedirectCallback(); + + // Volta para home (mantém a URL limpa) + window.history.replaceState({}, document.title, "/"); + setLoading(false); + return; + } + + const user = await userManager.getUser(); + + if (!user || user.expired) { + await userManager.signinRedirect(); + return; + } + + setLoading(false); + } catch (e) { + setError(e?.message ?? String(e)); + setLoading(false); + } + })(); + }, []); + + if (loading) { + return ( +
{error}
+
+