From 2805440f9f9b4f892a876bcaf386c180d06bd89b Mon Sep 17 00:00:00 2001 From: Artur Oliveira Date: Tue, 16 Dec 2025 13:48:47 -0300 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20adicionar=20fluxos=20de=20cri?= =?UTF-8?q?a=C3=A7=C3=A3o=20e=20perfil=20-=20Exporta=20utilit=C3=A1rios=20?= =?UTF-8?q?de=20token=20no=20cliente=20para=20buscar=20usu=C3=A1rio=20atua?= =?UTF-8?q?l=20-=20Implementa=20formul=C3=A1rios=20modais=20para=20novo=20?= =?UTF-8?q?servidor=20e=20edi=C3=A7=C3=A3o=20de=20perfil=20-=20Integra=20d?= =?UTF-8?q?ashboard=20com=20usu=C3=A1rio=20logado=20e=20atualiza=20lista?= =?UTF-8?q?=20ap=C3=B3s=20cria=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/Api.ts | 23 +- frontend/src/components/Header.tsx | 347 ++++++++++++++++++++++++++--- frontend/src/pages/Dashboard.tsx | 61 +++-- frontend/src/types/User.ts | 10 + 4 files changed, 386 insertions(+), 55 deletions(-) create mode 100644 frontend/src/types/User.ts diff --git a/frontend/src/Api.ts b/frontend/src/Api.ts index c374fd0..89c661d 100644 --- a/frontend/src/Api.ts +++ b/frontend/src/Api.ts @@ -13,11 +13,13 @@ const api = axios.create({ }, }); -type JwtPayload = { +export type JwtPayload = { exp?: number; + sub?: string; + [key: string]: unknown; }; -const getCookieValue = (name: string): string | undefined => { +export const getCookieValue = (name: string): string | undefined => { if (typeof document === "undefined") { return undefined; } @@ -30,7 +32,7 @@ const getCookieValue = (name: string): string | undefined => { return cookie ? decodeURIComponent(cookie.split("=")[1]) : undefined; }; -const decodeJwtPayload = (token: string): JwtPayload | null => { +export const decodeJwtPayload = (token: string): JwtPayload | null => { try { const [, payload] = token.split("."); if (!payload) return null; @@ -42,6 +44,15 @@ const decodeJwtPayload = (token: string): JwtPayload | null => { } }; +export const getAccessToken = () => getCookieValue(ACCESS_TOKEN_COOKIE); + +export const getCurrentUsername = () => { + const token = getAccessToken(); + if (!token) return undefined; + const payload = decodeJwtPayload(token); + return typeof payload?.sub === "string" ? payload.sub : undefined; +}; + const isTokenExpiringSoon = (token?: string) => { if (!token) return false; const payload = decodeJwtPayload(token); @@ -67,7 +78,7 @@ const refreshAccessToken = async () => { refreshPromise = api .post(REFRESH_ENDPOINT) .then(() => { - const token = getCookieValue(ACCESS_TOKEN_COOKIE); + const token = getAccessToken(); setAuthToken(token); }) .catch((error) => { @@ -85,7 +96,7 @@ api.interceptors.request.use(async (config) => { const isRefreshCall = config.url?.includes(REFRESH_ENDPOINT); if (!isRefreshCall) { - const token = getCookieValue(ACCESS_TOKEN_COOKIE); + const token = getAccessToken(); if (isTokenExpiringSoon(token)) { try { await refreshAccessToken(); @@ -95,7 +106,7 @@ api.interceptors.request.use(async (config) => { } } - const updatedToken = getCookieValue(ACCESS_TOKEN_COOKIE); + const updatedToken = getAccessToken(); if (updatedToken) { config.headers = config.headers ?? {}; config.headers.Authorization = `Bearer ${updatedToken}`; diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index d49cedf..d12307e 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -1,47 +1,312 @@ -import { useState } from "react"; +import { type ChangeEvent, type FormEvent, useEffect, useState } from "react"; +import toast from "react-hot-toast"; +import api from "../Api"; +import type { Applications, DatabaseType, ServersType } from "../types/enums"; +import type { User } from "../types/User"; -export const Header = () => { +type ModalType = "addServer" | "editProfile" | null; + +interface HeaderProps { + currentUser: User | null; + userError: string | null; + onServerCreated?: () => Promise | void; + onProfileUpdated?: (user: User) => void; +} + +type ServerFormState = { + name: string; + ip: string; + port: string; + user: string; + password: string; + type: ServersType; + application: Applications; + dbType: DatabaseType; +}; + +type ProfileFormState = { + firstName: string; + lastName: string; + email: string; + password: string; +}; + +const defaultServerForm: ServerFormState = { + name: "", + ip: "", + port: "", + user: "", + password: "", + type: "PRODUCTION", + application: "ASTERISK", + dbType: "MYSQL", +}; + +const defaultProfileForm: ProfileFormState = { + firstName: "", + lastName: "", + email: "", + password: "", +}; + +const serverTypeOptions: ServersType[] = ["PRODUCTION", "HOMOLOGATION", "DATABASE"]; +const applicationOptions: Applications[] = ["ASTERISK", "HITMANAGER", "HITMANAGER_V2", "OMNIHIT", "HITPHONE"]; +const databaseOptions: DatabaseType[] = ["MYSQL", "POSTGRESQL", "SQLSERVER", "ORACLE", "REDIS", "MONGODB", "MARIADB", "NONE"]; + +export const Header = ({ currentUser, userError, onServerCreated, onProfileUpdated }: HeaderProps) => { const [isMenuOpen, setMenuOpen] = useState(false); + const [activeModal, setActiveModal] = useState(null); + const [serverForm, setServerForm] = useState(defaultServerForm); + const [profileForm, setProfileForm] = useState(defaultProfileForm); + const [serverLoading, setServerLoading] = useState(false); + const [profileLoading, setProfileLoading] = useState(false); + + useEffect(() => { + if (currentUser) { + setProfileForm((prev) => ({ + ...prev, + firstName: currentUser.firstName ?? "", + lastName: currentUser.lastName ?? "", + email: currentUser.email ?? "", + })); + } + }, [currentUser]); const toggleMenu = () => setMenuOpen((prev) => !prev); + const openModal = (modal: ModalType) => { + setMenuOpen(false); + setActiveModal(modal); + }; + const closeModal = () => { + setActiveModal(null); + setServerForm(defaultServerForm); + setProfileForm((prev) => ({ ...prev, password: "" })); + }; + + const handleServerFormChange = (e: ChangeEvent) => { + const { name, value } = e.target; + setServerForm((prev) => ({ ...prev, [name]: value })); + }; + + const handleProfileFormChange = (e: ChangeEvent) => { + const { name, value } = e.target; + setProfileForm((prev) => ({ ...prev, [name]: value })); + }; + + const handleServerSubmit = async (event: FormEvent) => { + event.preventDefault(); + setServerLoading(true); + try { + await api.post("/api/servers", { + ...serverForm, + port: Number(serverForm.port), + }); + toast.success("Servidor criado com sucesso!"); + setServerForm(defaultServerForm); + setActiveModal(null); + if (onServerCreated) { + await Promise.resolve(onServerCreated()); + } + } catch (err: any) { + const message = err?.response?.data?.message || "Falha ao criar servidor."; + toast.error(message); + } finally { + setServerLoading(false); + } + }; + + const handleProfileSubmit = async (event: FormEvent) => { + event.preventDefault(); + if (!currentUser) { + toast.error("Usuário não identificado."); + return; + } + setProfileLoading(true); + try { + const { data } = await api.put(`/api/users/${currentUser.id}`, { + firstName: profileForm.firstName, + lastName: profileForm.lastName, + email: profileForm.email, + password: profileForm.password, + }); + toast.success("Perfil atualizado com sucesso!"); + setProfileForm((prev) => ({ ...prev, password: "" })); + setActiveModal(null); + onProfileUpdated?.(data); + } catch (err: any) { + const message = err?.response?.data?.message || "Falha ao atualizar o perfil."; + toast.error(message); + } finally { + setProfileLoading(false); + } + }; + + const renderModalContent = () => { + if (activeModal === "addServer") { + return ( +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ ); + } + + if (activeModal === "editProfile") { + if (userError) { + return

{userError}

; + } + + return ( +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +

Informe uma nova senha para confirmar a alteração.

+
+
+ + +
+
+ ); + } + + return null; + }; return ( -
-
- Logo Hit Communications -
-

Hit Communications

-

Servers Manager

+ <> +
+
+ Logo Hit Communications +
+

Hit Communications

+

Servers Manager

+
-
-
-
- + {isMenuOpen && ( +
+ + +
+ )} +
+ - {isMenuOpen && ( -
- -
+
+ + {activeModal && ( +
+
+
+

+ {activeModal === "addServer" ? "Adicionar novo servidor" : "Editar perfil"} +

+
- )} +
{renderModalContent()}
+
- - - + )} + ); }; @@ -53,7 +318,23 @@ const Styles = { subtitle: "text-xs uppercase tracking-wide text-text-secondary", actions: "flex items-center gap-3", menuTrigger: "rounded-lg border border-cardBorder bg-white/70 px-4 py-2 text-sm font-medium text-text flex items-center gap-2 hover:bg-white transition-colors", - dropdown: "absolute right-0 mt-2 w-44 rounded-lg border border-cardBorder bg-white py-2 shadow-lg", + dropdown: "absolute right-0 mt-2 w-48 rounded-lg border border-cardBorder bg-white py-2 shadow-lg z-10", dropdownItem: "w-full px-4 py-2 text-left text-sm text-text-secondary hover:bg-bg hover:text-text transition-colors", logoutButton: "rounded-md bg-accent px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-hover", + modalOverlay: "fixed inset-0 z-40 flex items-center justify-center bg-black/40 px-4", + modal: "w-full max-w-2xl rounded-2xl border border-cardBorder bg-card p-6 shadow-xl", + modalHeader: "flex items-start justify-between gap-4 pb-4 border-b border-cardBorder", + modalTitle: "text-lg font-semibold text-text", + closeButton: "text-2xl leading-none text-text-secondary hover:text-text", + modalBody: "pt-4", + form: "space-y-4", + formGrid: "grid gap-4 md:grid-cols-2", + field: "flex flex-col gap-2", + label: "text-xs font-semibold uppercase tracking-wide text-text-secondary", + input: "rounded-lg border border-cardBorder bg-white px-3 py-2 text-sm text-text outline-none focus:border-accent focus:ring-1 focus:ring-accent", + select: "rounded-lg border border-cardBorder bg-white px-3 py-2 text-sm text-text outline-none focus:border-accent focus:ring-1 focus:ring-accent capitalize", + helperText: "text-xs text-text-secondary", + modalActions: "flex justify-end gap-3 pt-2", + secondaryButton: "rounded-md border border-cardBorder px-4 py-2 text-sm font-medium text-text hover:bg-bg disabled:opacity-50", + primaryButton: "rounded-md bg-accent px-4 py-2 text-sm font-semibold text-white hover:bg-hover disabled:opacity-70", }; diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 5ef28aa..170e7c8 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,36 +1,65 @@ -import { useEffect, useState } from "react"; -import api from "../Api"; +import { useCallback, useEffect, useState } from "react"; +import api, { getCurrentUsername } from "../Api"; import { Layout } from "../components/Layout"; import { Header } from "../components/Header"; import { ServerCardMetrics } from "../components/ServerCardMetrics"; import { ServersTable } from "../components/ServersTable"; import type { Server } from "../types/Server"; +import type { User } from "../types/User"; +import toast from "react-hot-toast"; export const Dashboard = () => { const [servers, setServers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [currentUser, setCurrentUser] = useState(null); + const [userError, setUserError] = useState(null); + + const fetchServers = useCallback(async () => { + setLoading(true); + try { + const { data } = await api.get("/api/servers"); + setServers(data); + setError(null); + } catch (err: any) { + const message = err?.response?.data?.message || "Falha ao carregar servidores."; + setError(message); + } finally { + setLoading(false); + } + }, []); + + const fetchCurrentUser = useCallback(async () => { + try { + const username = getCurrentUsername(); + if (!username) { + setUserError("Não foi possível identificar o usuário logado."); + return; + } + const { data } = await api.get(`/api/users/username/${encodeURIComponent(username)}`); + setCurrentUser(data); + setUserError(null); + } catch (err: any) { + const message = err?.response?.data?.message || "Falha ao carregar o perfil do usuário."; + setUserError(message); + toast.error(message); + } + }, []); useEffect(() => { - const fetchServers = async () => { - try { - const { data } = await api.get("/api/servers"); - setServers(data); - } catch (err: any) { - const message = err?.response?.data?.message || "Falha ao carregar servidores."; - setError(message); - } finally { - setLoading(false); - } - }; - fetchServers(); - }, []); + fetchCurrentUser(); + }, [fetchServers, fetchCurrentUser]); return (
-
+
diff --git a/frontend/src/types/User.ts b/frontend/src/types/User.ts new file mode 100644 index 0000000..574f77a --- /dev/null +++ b/frontend/src/types/User.ts @@ -0,0 +1,10 @@ +export interface User { + id: string; + username: string; + firstName: string; + lastName: string; + email: string; + createdAt: string; + updatedAt: string; + lastLogin: string | null; +}