feat: migrate authentication to Logto and clear PWA cache
This commit is contained in:
138
package-lock.json
generated
138
package-lock.json
generated
@@ -8,6 +8,8 @@
|
|||||||
"name": "gpi-app",
|
"name": "gpi-app",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@logto/node": "^3.1.9",
|
||||||
|
"@logto/react": "^4.0.13",
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@types/mongoose": "^5.11.96",
|
"@types/mongoose": "^5.11.96",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
@@ -2337,6 +2339,63 @@
|
|||||||
"sisteransi": "^1.0.5"
|
"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": {
|
"node_modules/@mongodb-js/saslprep": {
|
||||||
"version": "1.4.5",
|
"version": "1.4.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -2798,6 +2857,16 @@
|
|||||||
"win32"
|
"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": {
|
"node_modules/@standard-schema/spec": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
@@ -4127,6 +4196,36 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001766",
|
"version": "1.0.30001766",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -6473,6 +6572,21 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"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": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
@@ -7018,6 +7132,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -7822,6 +7948,18 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/randombytes": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
"start": "node dist/server/index.js"
|
"start": "node dist/server/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@logto/node": "^3.1.9",
|
||||||
|
"@logto/react": "^4.0.13",
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@types/mongoose": "^5.11.96",
|
"@types/mongoose": "^5.11.96",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// App v1.5 - JWT Migration Final Check
|
|
||||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
|
import { Callback } from './pages/Callback';
|
||||||
import { AuthProvider, useAuth } from './context/AuthContext';
|
import { AuthProvider, useAuth } from './context/AuthContext';
|
||||||
import { SystemSettingsProvider } from './context/SystemSettingsContext';
|
import { SystemSettingsProvider } from './context/SystemSettingsContext';
|
||||||
import { NotificationProvider } from './contexts/NotificationContext';
|
import { NotificationProvider } from './contexts/NotificationContext';
|
||||||
@@ -103,11 +103,10 @@ const MainRouter: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
{!appUser ? (
|
<Routes>
|
||||||
<Login />
|
<Route path="/callback" element={<Callback />} />
|
||||||
) : (
|
<Route path="/*" element={!appUser ? <Login /> : <AppContent />} />
|
||||||
<AppContent />
|
</Routes>
|
||||||
)}
|
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||||
import type { AppUser } from '../types';
|
import type { AppUser } from '../types';
|
||||||
import { setApiToken, setApiOrganizationId, getBaseUrl } from '../services/api';
|
import { setApiToken, setApiOrganizationId, getBaseUrl } from '../services/api';
|
||||||
|
import { useLogto } from '@logto/react';
|
||||||
|
|
||||||
const API_URL = getBaseUrl();
|
const API_URL = getBaseUrl();
|
||||||
|
|
||||||
@@ -23,40 +24,55 @@ export interface AuthContextType {
|
|||||||
export const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
export const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
|
|
||||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const { isAuthenticated, getAccessToken, signOut, isLoading: isLogtoLoading } = useLogto();
|
||||||
const [appUser, setAppUser] = useState<AppUser | null>(null);
|
const [appUser, setAppUser] = useState<AppUser | null>(null);
|
||||||
const [token, setToken] = useState<string | null>(localStorage.getItem('jwt_token'));
|
const [token, setToken] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(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
|
// Initial load: se tem token, setar no interceptor e buscar dados do usuário
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (token) {
|
if (token) {
|
||||||
setApiToken(token);
|
|
||||||
refetchUser();
|
refetchUser();
|
||||||
} else {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
}
|
||||||
}, [token]);
|
}, [token]);
|
||||||
|
|
||||||
const login = useCallback((newToken: string, user: AppUser) => {
|
const login = useCallback((newToken: string, user: AppUser) => {
|
||||||
localStorage.setItem('jwt_token', newToken);
|
|
||||||
setToken(newToken);
|
setToken(newToken);
|
||||||
setAppUser(user);
|
setAppUser(user);
|
||||||
setApiToken(newToken);
|
setApiToken(newToken);
|
||||||
|
|
||||||
// Se a organização existir, setar o header
|
|
||||||
if (user.organizationId) {
|
if (user.organizationId) {
|
||||||
setApiOrganizationId(user.organizationId);
|
setApiOrganizationId(user.organizationId);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const logout = useCallback(() => {
|
const logout = useCallback(() => {
|
||||||
localStorage.removeItem('jwt_token');
|
|
||||||
setToken(null);
|
setToken(null);
|
||||||
setAppUser(null);
|
setAppUser(null);
|
||||||
setApiToken(null);
|
setApiToken(null);
|
||||||
setApiOrganizationId(null);
|
setApiOrganizationId(null);
|
||||||
}, []);
|
signOut(window.location.origin);
|
||||||
|
}, [signOut]);
|
||||||
|
|
||||||
const refetchUser = useCallback(async () => {
|
const refetchUser = useCallback(async () => {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|||||||
@@ -1,7 +1,26 @@
|
|||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import App from './App.tsx';
|
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(
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<LogtoProvider config={config}>
|
||||||
<App />
|
<App />
|
||||||
|
</LogtoProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
16
src/client/pages/Callback.tsx
Normal file
16
src/client/pages/Callback.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="flex h-screen items-center justify-center">
|
||||||
|
{isLoading ? 'Redirecionando...' : 'Concluído'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 = () => {
|
export const Login = () => {
|
||||||
const [email, setEmail] = useState("");
|
const { signIn, isAuthenticated } = useLogto();
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
const [errorMsg, setErrorMsg] = useState("");
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const { login } = useAuth();
|
const handleLogtoSignIn = () => {
|
||||||
|
signIn(window.location.origin + "/callback");
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen w-full flex items-center justify-center bg-surface-soft relative overflow-hidden">
|
<div className="min-h-screen w-full flex items-center justify-center bg-surface-soft relative overflow-hidden">
|
||||||
{/* Background decorative elements */}
|
|
||||||
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-primary/10 rounded-full blur-[120px] animate-pulse" />
|
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-primary/10 rounded-full blur-[120px] animate-pulse" />
|
||||||
<div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-primary/5 rounded-full blur-[120px]" />
|
<div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-primary/5 rounded-full blur-[120px]" />
|
||||||
|
|
||||||
<div className="relative z-10 w-full max-w-md px-6 flex flex-col items-center">
|
<div className="relative z-10 w-full max-w-md px-6 flex flex-col items-center">
|
||||||
{/* Logo Area */}
|
|
||||||
<div className="mb-8 flex flex-col items-center text-center">
|
<div className="mb-8 flex flex-col items-center text-center">
|
||||||
<div className="w-16 h-16 rounded-2xl bg-primary flex items-center justify-center text-white font-bold text-3xl shadow-2xl shadow-primary/40 mb-4 animate-in zoom-in duration-700">
|
<div className="w-16 h-16 rounded-2xl bg-primary flex items-center justify-center text-white font-bold text-3xl shadow-2xl shadow-primary/40 mb-4 animate-in zoom-in duration-700">
|
||||||
G
|
G
|
||||||
@@ -56,53 +23,15 @@ export const Login = () => {
|
|||||||
<p className="text-text-muted text-sm font-medium uppercase tracking-widest">Gestão de Pintura Industrial</p>
|
<p className="text-text-muted text-sm font-medium uppercase tracking-widest">Gestão de Pintura Industrial</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Custom Login Form */}
|
|
||||||
<div className="w-full bg-surface rounded-[2rem] border border-border/40 shadow-2xl shadow-primary/5 p-8 animate-in slide-in-from-bottom-8 duration-1000">
|
<div className="w-full bg-surface rounded-[2rem] border border-border/40 shadow-2xl shadow-primary/5 p-8 animate-in slide-in-from-bottom-8 duration-1000">
|
||||||
<h2 className="text-xl font-bold text-text-main mb-6 text-center">Entrar na sua conta</h2>
|
<h2 className="text-xl font-bold text-text-main mb-6 text-center">Autenticação</h2>
|
||||||
|
|
||||||
{errorMsg && (
|
|
||||||
<div className="mb-4 p-3 rounded-lg bg-error/10 border border-error/20 text-error text-sm text-center">
|
|
||||||
{errorMsg}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<label className="text-sm font-semibold text-text-secondary" htmlFor="email">Email</label>
|
|
||||||
<input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
required
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<label className="text-sm font-semibold text-text-secondary" htmlFor="password">Senha</label>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => 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="••••••••"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
onClick={handleLogtoSignIn}
|
||||||
disabled={loading}
|
className="w-full mt-4 bg-primary hover:bg-primary/90 text-white font-bold py-3 rounded-xl transition-all shadow-lg shadow-primary/20"
|
||||||
className="mt-4 bg-primary hover:bg-primary/90 text-white font-bold py-3 rounded-xl transition-all shadow-lg shadow-primary/20 disabled:opacity-70 disabled:cursor-not-allowed"
|
|
||||||
>
|
>
|
||||||
{loading ? "Entrando..." : "Entrar"}
|
{isAuthenticated ? "Autenticado (Redirecionando...)" : "Entrar com Logto"}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 flex items-center gap-2 text-text-muted/60 text-xs font-medium">
|
<div className="mt-8 flex items-center gap-2 text-text-muted/60 text-xs font-medium">
|
||||||
|
|||||||
Reference in New Issue
Block a user