From 8015b3ff77a815188bccad004d43a966fd231857 Mon Sep 17 00:00:00 2001 From: Gabriel Menezes Date: Sat, 10 Oct 2026 21:34:38 -0400 Subject: [PATCH] feat: add auth page with login/register and enable task3 CORS origin --- .../src/main/resources/application.properties | 2 +- docker-compose.yml | 6 +- new-frontend/package-lock.json | 80 +++- new-frontend/package.json | 3 +- new-frontend/src/App.tsx | 38 +- .../src/components/landing/CtaSection.tsx | 57 +-- new-frontend/src/components/landing/Hero.tsx | 5 +- .../src/components/landing/Navbar.tsx | 25 +- new-frontend/src/contexts/AuthContext.tsx | 186 ++++++++ new-frontend/src/lib/api.ts | 57 +++ new-frontend/src/pages/AuthPage.tsx | 409 ++++++++++++++++++ new-frontend/src/pages/LandingPage.tsx | 29 ++ new-frontend/src/services/authService.ts | 54 +++ 13 files changed, 856 insertions(+), 95 deletions(-) create mode 100644 new-frontend/src/contexts/AuthContext.tsx create mode 100644 new-frontend/src/lib/api.ts create mode 100644 new-frontend/src/pages/AuthPage.tsx create mode 100644 new-frontend/src/pages/LandingPage.tsx create mode 100644 new-frontend/src/services/authService.ts diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 290eab3..38589ce 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -1,7 +1,7 @@ quarkus.http.port=8081 quarkus.http.cors.enabled=true -quarkus.http.cors.origins=https://task2.beetech.dev +quarkus.http.cors.origins=https://task2.beetech.dev,https://task3.beetech.dev quarkus.http.cors.methods=GET,PUT,POST,DELETE,PATCH,OPTIONS quarkus.http.cors.exposed-headers=Content-Disposition quarkus.http.cors.access-control-max-age=24H diff --git a/docker-compose.yml b/docker-compose.yml index 0ecc5b0..8041ea5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -76,7 +76,7 @@ services: DB_USER: beetask DB_PASSWORD: beetask DB_URL: jdbc:postgresql://postgres:5432/beetask - QUARKUS_HTTP_CORS_ORIGINS: https://task2.beetech.dev + QUARKUS_HTTP_CORS_ORIGINS: "https://task2.beetech.dev,https://task3.beetech.dev" MP_JWT_VERIFY_ISSUER: ${MP_JWT_VERIFY_ISSUER} SMALLRYE_JWT_SIGN_KEY: ${SMALLRYE_JWT_SIGN_KEY} MP_JWT_VERIFY_PUBLICKEY: ${MP_JWT_VERIFY_PUBLICKEY} @@ -94,7 +94,7 @@ services: labels: - "traefik.enable=true" - "traefik.docker.network=proxy" - - "traefik.http.routers.backend.rule=Host(`api-task2.beetech.dev`)" + - "traefik.http.routers.backend.rule=Host(`api-task2.beetech.dev`) || Host(`api-task3.beetech.dev`)" - "traefik.http.routers.backend.entrypoints=web" - "traefik.http.services.backend.loadbalancer.server.port=8081" @@ -125,7 +125,7 @@ services: context: ./new-frontend dockerfile: Dockerfile args: - VITE_API_URL: ${VITE_API_URL:-https://api-task3.beetech.dev} + VITE_API_URL: ${VITE_API_URL:-https://api-task2.beetech.dev} container_name: bee-task-frontend2 restart: unless-stopped expose: diff --git a/new-frontend/package-lock.json b/new-frontend/package-lock.json index fbe6116..e16dd0c 100644 --- a/new-frontend/package-lock.json +++ b/new-frontend/package-lock.json @@ -15,7 +15,8 @@ "framer-motion": "^14.1.0", "lucide-react": "^1.55.0", "react": "^19.2.8", - "react-dom": "^19.2.8" + "react-dom": "^19.2.8", + "react-router-dom": "^7.18.4" }, "devDependencies": { "@tailwindcss/vite": "^4.3.3", @@ -1074,21 +1075,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/provider": { - "version": "3.11.1", - "resolved": "https://registry.npmjs.org/@react-spectrum/provider/-/provider-3.11.1.tgz", - "integrity": "sha512-TsoNdVdmlQ7L+75ILq5Yb3+wp/I1AtIeat0o+Y+ZBxP+TtWpwT1ZtCB5l3cplFVzHzOpZlzO0VaDrDP9ElGYDw==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@adobe/react-spectrum": "^3.47.0", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-stately/color": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/@react-stately/color/-/color-3.10.1.tgz", @@ -2065,7 +2051,7 @@ "version": "19.3.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -2075,7 +2061,7 @@ "version": "19.3.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.3.0" @@ -2290,6 +2276,19 @@ "integrity": "sha512-OXAPGFRNeLFnUfqDtloYdxkwsJoIdXe28+bjbpJiPqyei2HPa3VHmMCWa0Qe62+U4Ftf9Hj7hRssOkxz7WiWbg==", "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -3036,6 +3035,44 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-router": { + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz", + "integrity": "sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.4.tgz", + "integrity": "sha512-yrfmJHIpDG7taCpqKjT1G5B6q3O2K+RN8/fgNf0lTjCwiPbQ0ei6vXX9ZjQR+7ld8Tr7Z5xmyMnZ8YJrphWQUw==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.4" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/react-stately": { "version": "3.51.0", "resolved": "https://registry.npmjs.org/react-stately/-/react-stately-3.51.0.tgz", @@ -3118,6 +3155,12 @@ "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", "license": "MIT" }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/simple-swizzle": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", @@ -3173,6 +3216,7 @@ "version": "4.3.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, "license": "MIT" }, "node_modules/tapable": { diff --git a/new-frontend/package.json b/new-frontend/package.json index 768b43b..7c536a8 100644 --- a/new-frontend/package.json +++ b/new-frontend/package.json @@ -17,7 +17,8 @@ "framer-motion": "^14.1.0", "lucide-react": "^1.55.0", "react": "^19.2.8", - "react-dom": "^19.2.8" + "react-dom": "^19.2.8", + "react-router-dom": "^7.18.4" }, "devDependencies": { "@tailwindcss/vite": "^4.3.3", diff --git a/new-frontend/src/App.tsx b/new-frontend/src/App.tsx index 6f84862..b943950 100644 --- a/new-frontend/src/App.tsx +++ b/new-frontend/src/App.tsx @@ -1,30 +1,20 @@ -import { Navbar } from "./components/landing/Navbar" -import { Hero } from "./components/landing/Hero" -import { Stats } from "./components/landing/Stats" -import { Features } from "./components/landing/Features" -import { FeatureTabs } from "./components/landing/FeatureTabs" -import { Workflow } from "./components/landing/Workflow" -import { Testimonials } from "./components/landing/Testimonials" -import { Faq } from "./components/landing/Faq" -import { CtaSection } from "./components/landing/CtaSection" -import { Footer } from "./components/landing/Footer" +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' +import { AuthProvider } from './contexts/AuthContext' +import { LandingPage } from './pages/LandingPage' +import { AuthPage } from './pages/AuthPage' export function App() { return ( -
- -
- - - - - - - - -
-
-
+ + + + } /> + } /> + } /> + } /> + + + ) } diff --git a/new-frontend/src/components/landing/CtaSection.tsx b/new-frontend/src/components/landing/CtaSection.tsx index a39618b..606dec2 100644 --- a/new-frontend/src/components/landing/CtaSection.tsx +++ b/new-frontend/src/components/landing/CtaSection.tsx @@ -1,15 +1,16 @@ import { useState, type FormEvent } from "react" +import { useNavigate } from "react-router-dom" import { Button } from "@heroui/react" -import { ArrowRight, Sparkles, CheckCircle2, ShieldCheck } from "lucide-react" +import { ArrowRight, Sparkles, ShieldCheck } from "lucide-react" export const CtaSection = () => { const [email, setEmail] = useState("") - const [submitted, setSubmitted] = useState(false) + const navigate = useNavigate() const handleSubmit = (e: FormEvent) => { e.preventDefault() if (email) { - setSubmitted(true) + navigate(`/register?mode=register&email=${encodeURIComponent(email)}`) } } @@ -36,38 +37,26 @@ export const CtaSection = () => { organizar projetos e gerenciar seu squad com precisão cirúrgica.

- {submitted ? ( -
-
- - Workspace em preparação! -
-

- Enviamos o link de ativação para {email}. -

-
- ) : ( -
+ setEmail(e.target.value)} + className="w-full sm:w-80 px-4 py-3 rounded-xl bg-white/10 backdrop-blur-md border border-white/20 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-accent text-sm" + /> + -
- )} + Criar Conta + + +
diff --git a/new-frontend/src/components/landing/Hero.tsx b/new-frontend/src/components/landing/Hero.tsx index 276d24b..9d3c656 100644 --- a/new-frontend/src/components/landing/Hero.tsx +++ b/new-frontend/src/components/landing/Hero.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from "react" +import { Link } from "react-router-dom" import { Button, Card, @@ -80,12 +81,12 @@ export const Hero = () => { {/* Action Buttons */}
- + - + - +
diff --git a/new-frontend/src/contexts/AuthContext.tsx b/new-frontend/src/contexts/AuthContext.tsx new file mode 100644 index 0000000..17b57ab --- /dev/null +++ b/new-frontend/src/contexts/AuthContext.tsx @@ -0,0 +1,186 @@ +import { + createContext, + useContext, + useState, + useEffect, + useCallback, + useMemo, + type ReactNode, +} from 'react' +import { + authService, + type LoginResponse, + type CreateAccountResponse, + type OrganizationMembership, +} from '../services/authService' + +interface AuthContextType { + user: LoginResponse | null + isAuthenticated: boolean + isLoading: boolean + activeOrg: OrganizationMembership | null + login: (email: string, password: string) => Promise + register: (name: string, email: string, password: string) => Promise + logout: () => void + refreshUser: () => Promise + setActiveOrg: (id: number) => void +} + +const STORAGE_KEYS = { + USER: 'user', + ACTIVE_ORG_ID: 'activeOrgId', +} as const + +const AuthContext = createContext(undefined) + +export const AuthProvider = ({ children }: { children: ReactNode }) => { + const [user, setUser] = useState(null) + const [activeOrgId, setActiveOrgId] = useState(null) + const [isLoading, setIsLoading] = useState(true) + + const setupActiveOrg = useCallback((organizations: OrganizationMembership[]) => { + const storedOrgId = localStorage.getItem(STORAGE_KEYS.ACTIVE_ORG_ID) + const parsedStoredId = storedOrgId ? Number(storedOrgId) : null + + const isValidOrg = organizations.some((org) => org.id === parsedStoredId) + + if (isValidOrg && parsedStoredId !== null) { + setActiveOrgId(parsedStoredId) + } else if (organizations.length > 0) { + const firstOrgId = organizations[0].id + setActiveOrgId(firstOrgId) + localStorage.setItem(STORAGE_KEYS.ACTIVE_ORG_ID, firstOrgId.toString()) + } + }, []) + + const logout = useCallback(() => { + localStorage.removeItem(STORAGE_KEYS.USER) + localStorage.removeItem(STORAGE_KEYS.ACTIVE_ORG_ID) + setUser(null) + setActiveOrgId(null) + }, []) + + const renewToken = useCallback(async (refreshToken: string) => { + try { + const response = await authService.refresh(refreshToken) + response.issuedAt = new Date().toISOString() + localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(response)) + setUser(response) + } catch { + logout() + } + }, [logout]) + + const checkTokenExpiration = useCallback( + (userData: LoginResponse) => { + const issuedAt = new Date(userData.issuedAt).getTime() + const expiresAt = issuedAt + userData.expiresIn * 1000 + const now = Date.now() + + if (now >= expiresAt) { + logout() + return false + } + + // Se expirar em menos de 5 min, renova + if (expiresAt - now < 300000) { + renewToken(userData.refreshToken) + } + + return true + }, + [logout, renewToken] + ) + + useEffect(() => { + const storedUser = localStorage.getItem(STORAGE_KEYS.USER) + if (storedUser) { + try { + const userData: LoginResponse = JSON.parse(storedUser) + if (checkTokenExpiration(userData)) { + setUser(userData) + setupActiveOrg(userData.organizations) + } + } catch { + localStorage.removeItem(STORAGE_KEYS.USER) + } + } + setIsLoading(false) + }, [checkTokenExpiration, setupActiveOrg]) + + // Verificação periódica a cada 60 segundos + useEffect(() => { + if (!user) return + + const interval = setInterval(() => { + checkTokenExpiration(user) + }, 60000) + + return () => clearInterval(interval) + }, [user, checkTokenExpiration]) + + const login = async (email: string, password: string) => { + const response = await authService.login(email, password) + response.issuedAt = new Date().toISOString() + localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(response)) + setUser(response) + setupActiveOrg(response.organizations) + return response + } + + const register = async (name: string, email: string, password: string) => { + return authService.register(name, email, password) + } + + const refreshUser = async () => { + try { + const response = await authService.me() + const storedUser = localStorage.getItem(STORAGE_KEYS.USER) + if (storedUser) { + const userData = JSON.parse(storedUser) + const updatedUser = { ...userData, ...response } + localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(updatedUser)) + setUser(updatedUser) + setupActiveOrg(updatedUser.organizations) + } + } catch { + // Ignora erro + } + } + + const setActiveOrg = (id: number) => { + setActiveOrgId(id) + localStorage.setItem(STORAGE_KEYS.ACTIVE_ORG_ID, id.toString()) + } + + const activeOrg = useMemo( + () => user?.organizations.find((org) => org.id === activeOrgId) || null, + [user, activeOrgId] + ) + + return ( + + {children} + + ) +} + +export const useAuth = () => { + const context = useContext(AuthContext) + if (!context) { + throw new Error('useAuth must be used within an AuthProvider') + } + return context +} diff --git a/new-frontend/src/lib/api.ts b/new-frontend/src/lib/api.ts new file mode 100644 index 0000000..6a58420 --- /dev/null +++ b/new-frontend/src/lib/api.ts @@ -0,0 +1,57 @@ +const API_BASE_URL = (import.meta.env.VITE_API_URL as string | undefined) ?? 'http://localhost:8081' + +function buildUrl(path: string) { + return `${API_BASE_URL}${path}` +} + +type RequestOptions = Omit & { + body?: unknown + skipAuth?: boolean +} + +export async function apiFetch(path: string, options: RequestOptions = {}): Promise { + const { body, headers, skipAuth, ...rest } = options + + let authHeader: Record = {} + if (!skipAuth) { + const storedUser = localStorage.getItem('user') + if (storedUser) { + try { + const userData = JSON.parse(storedUser) + if (userData?.jwt) { + authHeader = { Authorization: `Bearer ${userData.jwt}` } + } + } catch { + // Ignora erro de parsing + } + } + } + + const isFormData = body instanceof FormData + const defaultHeaders: Record = isFormData ? {} : { 'Content-Type': 'application/json' } + + const response = await fetch(buildUrl(path), { + ...rest, + headers: { + ...defaultHeaders, + ...authHeader, + ...((headers as Record) || {}), + }, + body: isFormData ? body : (body !== undefined && body !== null ? JSON.stringify(body) : undefined), + }) + + const text = await response.text() + if (!response.ok) { + let errorMessage = text || 'Não foi possível concluir a requisição.' + try { + const parsed = JSON.parse(text) + if (parsed.message) errorMessage = parsed.message + if (parsed.error) errorMessage = parsed.error + } catch { + // Usar texto puro + } + throw new Error(errorMessage) + } + + return text ? (JSON.parse(text) as T) : (undefined as T) +} diff --git a/new-frontend/src/pages/AuthPage.tsx b/new-frontend/src/pages/AuthPage.tsx new file mode 100644 index 0000000..06d2ccf --- /dev/null +++ b/new-frontend/src/pages/AuthPage.tsx @@ -0,0 +1,409 @@ +import { useState, type FormEvent } from 'react' +import { Link, useNavigate, useSearchParams } from 'react-router-dom' +import { + Card, + CardHeader, + CardContent, + CardFooter, + Button, + Input, + Chip, + ChipLabel, + Alert, + AlertTitle, + AlertDescription, +} from '@heroui/react' +import { + Mail, + Lock, + User, + ArrowRight, + Eye, + EyeOff, + ArrowLeft, + ShieldCheck, +} from 'lucide-react' +import { useAuth } from '../contexts/AuthContext' + +export const AuthPage = () => { + const [searchParams, setSearchParams] = useSearchParams() + const initialMode = searchParams.get('mode') === 'register' ? 'register' : 'login' + const initialEmail = searchParams.get('email') || '' + const [mode, setMode] = useState<'login' | 'register'>(initialMode) + + // Form states + const [name, setName] = useState('') + const [email, setEmail] = useState(initialEmail) + const [password, setPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [showPassword, setShowPassword] = useState(false) + + // Status & UI + const [isLoading, setIsLoading] = useState(false) + const [errorMessage, setErrorMessage] = useState(null) + const [successMessage, setSuccessMessage] = useState(null) + + const { login, register } = useAuth() + const navigate = useNavigate() + + const switchMode = (newMode: 'login' | 'register') => { + setMode(newMode) + setSearchParams({ mode: newMode }) + setErrorMessage(null) + setSuccessMessage(null) + } + + const handleLogin = async (e: FormEvent) => { + e.preventDefault() + setErrorMessage(null) + setIsLoading(true) + + try { + const response = await login(email, password) + if (!response.organizations || response.organizations.length === 0) { + navigate('/organizations') + } else { + navigate('/dashboard') + } + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : 'Erro ao realizar login. Verifique suas credenciais.' + setErrorMessage(msg) + } finally { + setIsLoading(false) + } + } + + const handleRegister = async (e: FormEvent) => { + e.preventDefault() + setErrorMessage(null) + + if (password.length < 6) { + setErrorMessage('A senha deve conter no mínimo 6 caracteres.') + return + } + + if (password !== confirmPassword) { + setErrorMessage('As senhas digitadas não coincidem.') + return + } + + setIsLoading(true) + + try { + await register(name, email, password) + setSuccessMessage('Conta criada com sucesso! Faça login para acessar seu workspace.') + setMode('login') + setSearchParams({ mode: 'login' }) + setPassword('') + setConfirmPassword('') + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : 'Erro ao criar conta. Tente novamente.' + setErrorMessage(msg) + } finally { + setIsLoading(false) + } + } + + return ( +
+ {/* Decorative ambient gradients */} +
+
+
+
+ + {/* Top back navigation */} +
+ + + Voltar para a página inicial + + + v3.0 + +
+ + {/* Main Auth Card */} + + {/* Card Header & Brand */} + +
+ 🐝 +
+ +
+

+ BeeTask +

+

+ {mode === 'login' + ? 'Acesse seu painel com segurança e foco' + : 'Crie seu workspace corporativo em segundos'} +

+
+ + {/* Mode Switcher Tabs */} +
+ + +
+
+ + {/* Feedback Alerts */} + + {errorMessage && ( + +
+ Atenção + {errorMessage} +
+
+ )} + + {successMessage && ( + +
+ Sucesso + {successMessage} +
+
+ )} + + {/* Mode Form */} + {mode === 'login' ? ( +
+
+ +
+
+ +
+ setEmail(e.target.value)} + placeholder="seu.email@empresa.com" + className="w-full pl-10 pr-4 py-2.5 bg-surface-muted/60 border border-border-soft rounded-xl text-sm text-text-main placeholder-text-muted/60 focus:outline-none focus:ring-2 focus:ring-brand/20 focus:border-brand transition-all" + /> +
+
+ +
+ +
+
+ +
+ setPassword(e.target.value)} + placeholder="••••••••" + className="w-full pl-10 pr-10 py-2.5 bg-surface-muted/60 border border-border-soft rounded-xl text-sm text-text-main placeholder-text-muted/60 focus:outline-none focus:ring-2 focus:ring-brand/20 focus:border-brand transition-all font-mono" + /> + +
+
+ + +
+ ) : ( +
+
+ +
+
+ +
+ setName(e.target.value)} + placeholder="Ex: Carlos Oliveira" + className="w-full pl-10 pr-4 py-2.5 bg-surface-muted/60 border border-border-soft rounded-xl text-sm text-text-main placeholder-text-muted/60 focus:outline-none focus:ring-2 focus:ring-brand/20 focus:border-brand transition-all" + /> +
+
+ +
+ +
+
+ +
+ setEmail(e.target.value)} + placeholder="seu.email@empresa.com" + className="w-full pl-10 pr-4 py-2.5 bg-surface-muted/60 border border-border-soft rounded-xl text-sm text-text-main placeholder-text-muted/60 focus:outline-none focus:ring-2 focus:ring-brand/20 focus:border-brand transition-all" + /> +
+
+ +
+ +
+
+ +
+ setPassword(e.target.value)} + placeholder="Mínimo 6 dígitos" + className="w-full pl-10 pr-10 py-2.5 bg-surface-muted/60 border border-border-soft rounded-xl text-sm text-text-main placeholder-text-muted/60 focus:outline-none focus:ring-2 focus:ring-brand/20 focus:border-brand transition-all font-mono" + /> + +
+
+ +
+ +
+
+ +
+ setConfirmPassword(e.target.value)} + placeholder="Repita a senha" + className="w-full pl-10 pr-4 py-2.5 bg-surface-muted/60 border border-border-soft rounded-xl text-sm text-text-main placeholder-text-muted/60 focus:outline-none focus:ring-2 focus:ring-brand/20 focus:border-brand transition-all font-mono" + /> +
+
+ + +
+ )} +
+ + {/* Footer Links & Info */} + +

+ {mode === 'login' ? ( + <> + Ainda não tem conta no Bee Task?{' '} + + + ) : ( + <> + Já possui uma conta ativa?{' '} + + + )} +

+ +
+ + Autenticação segura com JWT Quarkus & MinIO S3 +
+
+
+
+ ) +} diff --git a/new-frontend/src/pages/LandingPage.tsx b/new-frontend/src/pages/LandingPage.tsx new file mode 100644 index 0000000..3146ec6 --- /dev/null +++ b/new-frontend/src/pages/LandingPage.tsx @@ -0,0 +1,29 @@ +import { Navbar } from "../components/landing/Navbar" +import { Hero } from "../components/landing/Hero" +import { Stats } from "../components/landing/Stats" +import { Features } from "../components/landing/Features" +import { FeatureTabs } from "../components/landing/FeatureTabs" +import { Workflow } from "../components/landing/Workflow" +import { Testimonials } from "../components/landing/Testimonials" +import { Faq } from "../components/landing/Faq" +import { CtaSection } from "../components/landing/CtaSection" +import { Footer } from "../components/landing/Footer" + +export const LandingPage = () => { + return ( +
+ +
+ + + + + + + + +
+
+
+ ) +} diff --git a/new-frontend/src/services/authService.ts b/new-frontend/src/services/authService.ts new file mode 100644 index 0000000..74e6632 --- /dev/null +++ b/new-frontend/src/services/authService.ts @@ -0,0 +1,54 @@ +import { apiFetch } from '../lib/api' + +export type OrganizationMembership = { + id: number + name: string + roles: string[] +} + +export type LoginResponse = { + name: string + email: string + photo: string + jwt: string + refreshToken: string + expiresIn: number // seconds + issuedAt: string // ISO date + organizations: OrganizationMembership[] +} + +export type CreateAccountResponse = { + id: number + name: string + email: string +} + +export const authService = { + async login(email: string, password: string): Promise { + return apiFetch('/auth/login', { + method: 'POST', + body: { email, password }, + skipAuth: true, + }) + }, + + async register(name: string, email: string, password: string): Promise { + return apiFetch('/auth/register', { + method: 'POST', + body: { name, email, password }, + skipAuth: true, + }) + }, + + async refresh(refreshToken: string): Promise { + return apiFetch('/auth/refresh', { + method: 'POST', + body: { refreshToken }, + skipAuth: true, + }) + }, + + async me(): Promise { + return apiFetch('/auth/me') + }, +}