From 778d6d18eed1561973b2c7401b566c1c1f77bf20 Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Thu, 19 Mar 2026 15:51:09 +0000 Subject: [PATCH] feat: migrate authentication to Logto and clear PWA cache --- package-lock.json | 138 +++++++++++++++++++++++++++++ package.json | 2 + src/client/App.tsx | 11 ++- src/client/context/AuthContext.tsx | 32 +++++-- src/client/main.tsx | 21 ++++- src/client/pages/Callback.tsx | 16 ++++ src/client/pages/Login.tsx | 95 +++----------------- 7 files changed, 217 insertions(+), 98 deletions(-) create mode 100644 src/client/pages/Callback.tsx diff --git a/package-lock.json b/package-lock.json index 3a983d4..4ffa0a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "gpi-app", "version": "1.0.0", "dependencies": { + "@logto/node": "^3.1.9", + "@logto/react": "^4.0.13", "@tailwindcss/postcss": "^4.1.18", "@types/mongoose": "^5.11.96", "@types/uuid": "^10.0.0", @@ -2337,6 +2339,63 @@ "sisteransi": "^1.0.5" } }, + "node_modules/@logto/browser": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@logto/browser/-/browser-3.0.12.tgz", + "integrity": "sha512-Ec45IExLYS64bF22wS7dZuWgOMmC2w3FZmWWnVCv2fX2vKQVs0wiI+FE/PlNhEvi8up4AW0zHO4NTGwF7ipFsQ==", + "license": "MIT", + "dependencies": { + "@logto/client": "^3.1.7", + "@silverhand/essentials": "^2.9.3", + "js-base64": "^3.7.4" + } + }, + "node_modules/@logto/client": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@logto/client/-/client-3.1.7.tgz", + "integrity": "sha512-t/5wXMhiXtmbmP6Cmcl4uMsYetq21vSZuYZztPHXv6QX0dx7lSKBvYi/65ERoS+fmNmtV2/i4Ojf1U41o0TLPQ==", + "license": "MIT", + "dependencies": { + "@logto/js": "^6.1.1", + "@silverhand/essentials": "^2.9.3", + "camelcase-keys": "^9.1.3", + "jose": "^5.2.2" + } + }, + "node_modules/@logto/js": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@logto/js/-/js-6.1.1.tgz", + "integrity": "sha512-G0lRS7VyOXdB06WYajEh9Kq2E3m11JshiKIKLj6LRPI1qZ06JYQ+Jsej3K60/4OIZMSzUas4FVnY+ORrhDdktA==", + "license": "MIT", + "dependencies": { + "@silverhand/essentials": "^2.9.3", + "camelcase-keys": "^9.1.3" + } + }, + "node_modules/@logto/node": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@logto/node/-/node-3.1.9.tgz", + "integrity": "sha512-ApbUf3tWZYtMt6KJZo+bfms+5WcR7Cuz3dE9mVoPuo/joA08aU18fSe3L7VXqFu0nUJnG8BZi7ngoqCJEkQTig==", + "license": "MIT", + "dependencies": { + "@logto/client": "^3.1.7", + "@silverhand/essentials": "^2.9.3", + "js-base64": "^3.7.4" + } + }, + "node_modules/@logto/react": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/@logto/react/-/react-4.0.13.tgz", + "integrity": "sha512-CU4rjJmueY0CQoJZq7BDZt/9sQYpxKDwVBrGHR55ljl4zPFF2URJPixqCtEEfWq5/pFk7MEnIOePOYbj7BWKfQ==", + "license": "MIT", + "dependencies": { + "@logto/browser": "^3.0.12", + "@silverhand/essentials": "^2.9.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@mongodb-js/saslprep": { "version": "1.4.5", "license": "MIT", @@ -2798,6 +2857,16 @@ "win32" ] }, + "node_modules/@silverhand/essentials": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/@silverhand/essentials/-/essentials-2.9.3.tgz", + "integrity": "sha512-OM9pyGc/yYJMVQw+fFOZZaTHXDWc45sprj+ky+QjC9inhf5w51L1WBmzAwFuYkHAwO1M19fxVf2sTH9KKP48yg==", + "license": "MIT", + "engines": { + "node": ">=18.12.0", + "pnpm": "^10.0.0" + } + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "license": "MIT" @@ -4127,6 +4196,36 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-9.1.3.tgz", + "integrity": "sha512-Rircqi9ch8AnZscQcsA1C47NFdaO3wukpmIRzYcDOrmvgt78hM/sj5pZhZNec2NM12uk5vTwRHZ4anGcrC4ZTg==", + "license": "MIT", + "dependencies": { + "camelcase": "^8.0.0", + "map-obj": "5.0.0", + "quick-lru": "^6.1.1", + "type-fest": "^4.3.2" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001766", "dev": true, @@ -6473,6 +6572,21 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-base64": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", + "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", + "license": "BSD-3-Clause" + }, "node_modules/js-tokens": { "version": "4.0.0", "license": "MIT" @@ -7018,6 +7132,18 @@ "dev": true, "license": "ISC" }, + "node_modules/map-obj": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-5.0.0.tgz", + "integrity": "sha512-2L3MIgJynYrZ3TYMriLDLWocz15okFakV6J12HXvMXDHui2x/zgChzg1u9mFFGbbGWE+GsLpQByt4POb9Or+uA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "license": "MIT", @@ -7822,6 +7948,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quick-lru": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz", + "integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", diff --git a/package.json b/package.json index 2f21af9..8888c83 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "start": "node dist/server/index.js" }, "dependencies": { + "@logto/node": "^3.1.9", + "@logto/react": "^4.0.13", "@tailwindcss/postcss": "^4.1.18", "@types/mongoose": "^5.11.96", "@types/uuid": "^10.0.0", diff --git a/src/client/App.tsx b/src/client/App.tsx index 2c9c64c..385084e 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -1,5 +1,5 @@ -// App v1.5 - JWT Migration Final Check import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; +import { Callback } from './pages/Callback'; import { AuthProvider, useAuth } from './context/AuthContext'; import { SystemSettingsProvider } from './context/SystemSettingsContext'; import { NotificationProvider } from './contexts/NotificationContext'; @@ -103,11 +103,10 @@ const MainRouter: React.FC = () => { return ( - {!appUser ? ( - - ) : ( - - )} + + } /> + : } /> + ); }; diff --git a/src/client/context/AuthContext.tsx b/src/client/context/AuthContext.tsx index 3a83cad..392f1e3 100644 --- a/src/client/context/AuthContext.tsx +++ b/src/client/context/AuthContext.tsx @@ -1,6 +1,7 @@ import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; import type { AppUser } from '../types'; import { setApiToken, setApiOrganizationId, getBaseUrl } from '../services/api'; +import { useLogto } from '@logto/react'; const API_URL = getBaseUrl(); @@ -23,40 +24,55 @@ export interface AuthContextType { export const AuthContext = createContext(undefined); export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const { isAuthenticated, getAccessToken, signOut, isLoading: isLogtoLoading } = useLogto(); const [appUser, setAppUser] = useState(null); - const [token, setToken] = useState(localStorage.getItem('jwt_token')); + const [token, setToken] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); + useEffect(() => { + const fetchTokenAndUser = async () => { + if (isAuthenticated) { + try { + const accessToken = await getAccessToken(); + if (accessToken) { + setToken(accessToken); + setApiToken(accessToken); + } + } catch (err) { + console.error(err); + } + } else if (!isLogtoLoading) { + setIsLoading(false); + } + }; + fetchTokenAndUser(); + }, [isAuthenticated, isLogtoLoading, getAccessToken]); + // Initial load: se tem token, setar no interceptor e buscar dados do usuário useEffect(() => { if (token) { - setApiToken(token); refetchUser(); - } else { - setIsLoading(false); } }, [token]); const login = useCallback((newToken: string, user: AppUser) => { - localStorage.setItem('jwt_token', newToken); setToken(newToken); setAppUser(user); setApiToken(newToken); - // Se a organização existir, setar o header if (user.organizationId) { setApiOrganizationId(user.organizationId); } }, []); const logout = useCallback(() => { - localStorage.removeItem('jwt_token'); setToken(null); setAppUser(null); setApiToken(null); setApiOrganizationId(null); - }, []); + signOut(window.location.origin); + }, [signOut]); const refetchUser = useCallback(async () => { if (!token) return; diff --git a/src/client/main.tsx b/src/client/main.tsx index e491130..e9a845b 100644 --- a/src/client/main.tsx +++ b/src/client/main.tsx @@ -1,7 +1,26 @@ import { createRoot } from 'react-dom/client'; import './index.css'; import App from './App.tsx'; +import { LogtoProvider } from '@logto/react'; +import type { LogtoConfig } from '@logto/react'; + +// Require the user to define VITE_LOGTO_APP_ID in Coolify +const config: LogtoConfig = { + endpoint: 'https://logto.reifonas.cloud', + appId: import.meta.env.VITE_LOGTO_APP_ID || '', // Replace or add via Envs! +}; + +// Force service worker cache clearing because of persistent Clerk error +if ('serviceWorker' in navigator) { + navigator.serviceWorker.getRegistrations().then(function(registrations) { + for(let registration of registrations) { + registration.unregister(); + } + }); +} createRoot(document.getElementById('root')!).render( - + + + ); diff --git a/src/client/pages/Callback.tsx b/src/client/pages/Callback.tsx new file mode 100644 index 0000000..7774716 --- /dev/null +++ b/src/client/pages/Callback.tsx @@ -0,0 +1,16 @@ +import { useHandleSignInCallback } from '@logto/react'; +import { useNavigate } from 'react-router-dom'; + +export const Callback = () => { + const navigate = useNavigate(); + const { isLoading } = useHandleSignInCallback(() => { + // Done, navigate to home + navigate('/'); + }); + + return ( +
+ {isLoading ? 'Redirecionando...' : 'Concluído'} +
+ ); +}; diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login.tsx index 4ff78a1..0a690cc 100644 --- a/src/client/pages/Login.tsx +++ b/src/client/pages/Login.tsx @@ -1,53 +1,20 @@ -import React, { useState } from "react"; -import { Hammer } from "lucide-react"; -import { useAuth } from "../context/useAuth"; -import { getBaseUrl } from "../services/api"; -const API_URL = getBaseUrl(); +import { Hammer } from "lucide-react"; +import { useLogto } from "@logto/react"; export const Login = () => { - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [errorMsg, setErrorMsg] = useState(""); - const [loading, setLoading] = useState(false); - - const { login } = useAuth(); + const { signIn, isAuthenticated } = useLogto(); - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setErrorMsg(""); - setLoading(true); - - try { - const response = await fetch(`${API_URL}/auth/login`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email, password }) - }); - - const data = await response.json(); - - if (!response.ok) { - setErrorMsg(data.error || "Erro ao efetuar login"); - setLoading(false); - return; - } - - login(data.token, data.user); - } catch (err) { - setErrorMsg("Falha na conexão com o servidor."); - setLoading(false); - } + const handleLogtoSignIn = () => { + signIn(window.location.origin + "/callback"); }; return (
- {/* Background decorative elements */}
- {/* Logo Area */}
G @@ -56,53 +23,15 @@ export const Login = () => {

Gestão de Pintura Industrial

- {/* Custom Login Form */}
-

Entrar na sua conta

+

Autenticação

- {errorMsg && ( -
- {errorMsg} -
- )} - -
-
- - setEmail(e.target.value)} - className="bg-surface-soft border border-border/40 focus:ring-2 focus:ring-primary/20 focus:border-primary rounded-xl px-4 py-3 text-text-main outline-none transition-all" - placeholder="seu@email.com" - /> -
- -
-
- -
- setPassword(e.target.value)} - className="bg-surface-soft border border-border/40 focus:ring-2 focus:ring-primary/20 focus:border-primary rounded-xl px-4 py-3 text-text-main outline-none transition-all" - placeholder="••••••••" - /> -
- - -
+