From fcd4ebc608e7d0e9f6f0eb106ba7f53177d28d05 Mon Sep 17 00:00:00 2001
From: Claude
Date: Mon, 17 Nov 2025 19:43:50 +0000
Subject: [PATCH] =?UTF-8?q?feat:=20melhorar=20UX/UI=20e=20seguran=C3=A7a?=
=?UTF-8?q?=20do=20m=C3=B3dulo=20de=20ajustes=20de=20usu=C3=A1rio?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Implementa melhorias abrangentes de UX, UI e segurança nos formulários
de alteração de senha e e-mail:
✨ Funcionalidades adicionadas:
- Validação em tempo real para campos duplicados (senha e e-mail)
- Campo de senha atual obrigatório para alterações de senha
- Campo de senha para confirmar identidade ao alterar e-mail
- Detecção de método de autenticação (Google OAuth vs Email/Senha)
- Indicador de força de senha com feedback visual
- Bloqueio de alteração de senha para usuários Google OAuth
🎨 Melhorias de UI:
- Feedback visual instantâneo com ícones de check/close
- Bordas coloridas indicando status de validação (verde/vermelho)
- Mensagens de erro claras e específicas em tempo real
- Alerta amigável para usuários Google OAuth
- Indicador de progresso de força de senha
🔒 Segurança:
- Validação de senha atual no backend usando Better Auth
- Prevenção de alteração para o mesmo e-mail
- Verificação de e-mails duplicados no sistema
- Bloqueio de submissão quando validações falham
♿ Acessibilidade:
- Atributos aria-label, aria-required, aria-invalid
- role="alert" para mensagens de erro
- aria-describedby para textos auxiliares
- Labels descritivas e navegação por teclado aprimorada
🐛 Correções:
- Corrigido uso de error.errors para error.issues no Zod
- Validação backend de senha atual implementada
- Mensagens de erro específicas (não genéricas)
Ref: Análise completa de UX/UI solicitada para módulo de ajustes
---
app/(dashboard)/ajustes/actions.ts | 90 ++++++++-
app/(dashboard)/ajustes/page.tsx | 14 +-
components/ajustes/update-email-form.tsx | 174 ++++++++++++++++--
components/ajustes/update-password-form.tsx | 191 +++++++++++++++++++-
4 files changed, 436 insertions(+), 33 deletions(-)
diff --git a/app/(dashboard)/ajustes/actions.ts b/app/(dashboard)/ajustes/actions.ts
index 457abda..a29f1fe 100644
--- a/app/(dashboard)/ajustes/actions.ts
+++ b/app/(dashboard)/ajustes/actions.ts
@@ -22,6 +22,7 @@ const updateNameSchema = z.object({
const updatePasswordSchema = z
.object({
+ currentPassword: z.string().min(1, "Senha atual é obrigatória"),
newPassword: z.string().min(6, "A senha deve ter no mínimo 6 caracteres"),
confirmPassword: z.string(),
})
@@ -32,6 +33,7 @@ const updatePasswordSchema = z
const updateEmailSchema = z
.object({
+ password: z.string().optional(), // Opcional para usuários Google OAuth
newEmail: z.string().email("E-mail inválido"),
confirmEmail: z.string().email("E-mail inválido"),
})
@@ -82,7 +84,7 @@ export async function updateNameAction(
if (error instanceof z.ZodError) {
return {
success: false,
- error: error.errors[0]?.message || "Dados inválidos",
+ error: error.issues[0]?.message || "Dados inválidos",
};
}
@@ -111,12 +113,27 @@ export async function updatePasswordAction(
const validated = updatePasswordSchema.parse(data);
+ // Verificar se o usuário tem conta com provedor Google
+ const userAccount = await db.query.account.findFirst({
+ where: and(
+ eq(schema.account.userId, session.user.id),
+ eq(schema.account.providerId, "google")
+ ),
+ });
+
+ if (userAccount) {
+ return {
+ success: false,
+ error: "Não é possível alterar senha para contas autenticadas via Google",
+ };
+ }
+
// Usar a API do Better Auth para atualizar a senha
try {
await auth.api.changePassword({
body: {
newPassword: validated.newPassword,
- currentPassword: "", // Better Auth pode não exigir a senha atual dependendo da configuração
+ currentPassword: validated.currentPassword,
},
headers: await headers(),
});
@@ -125,19 +142,27 @@ export async function updatePasswordAction(
success: true,
message: "Senha atualizada com sucesso",
};
- } catch (authError) {
+ } catch (authError: any) {
console.error("Erro na API do Better Auth:", authError);
- // Se a API do Better Auth falhar, retornar erro genérico
+
+ // Verificar se o erro é de senha incorreta
+ if (authError?.message?.includes("password") || authError?.message?.includes("incorrect")) {
+ return {
+ success: false,
+ error: "Senha atual incorreta",
+ };
+ }
+
return {
success: false,
- error: "Erro ao atualizar senha. Tente novamente.",
+ error: "Erro ao atualizar senha. Verifique se a senha atual está correta.",
};
}
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
- error: error.errors[0]?.message || "Dados inválidos",
+ error: error.issues[0]?.message || "Dados inválidos",
};
}
@@ -157,7 +182,7 @@ export async function updateEmailAction(
headers: await headers(),
});
- if (!session?.user?.id) {
+ if (!session?.user?.id || !session?.user?.email) {
return {
success: false,
error: "Não autenticado",
@@ -166,6 +191,45 @@ export async function updateEmailAction(
const validated = updateEmailSchema.parse(data);
+ // Verificar se o usuário tem conta com provedor Google
+ const userAccount = await db.query.account.findFirst({
+ where: and(
+ eq(schema.account.userId, session.user.id),
+ eq(schema.account.providerId, "google")
+ ),
+ });
+
+ const isGoogleAuth = !!userAccount;
+
+ // Se não for Google OAuth, validar senha
+ if (!isGoogleAuth) {
+ if (!validated.password) {
+ return {
+ success: false,
+ error: "Senha é obrigatória para confirmar a alteração",
+ };
+ }
+
+ // Validar senha tentando fazer changePassword para a mesma senha
+ // Se falhar, a senha atual está incorreta
+ try {
+ await auth.api.changePassword({
+ body: {
+ newPassword: validated.password,
+ currentPassword: validated.password,
+ },
+ headers: await headers(),
+ });
+ } catch (authError: any) {
+ // Se der erro é porque a senha está incorreta
+ console.error("Erro ao validar senha:", authError);
+ return {
+ success: false,
+ error: "Senha incorreta",
+ };
+ }
+ }
+
// Verificar se o e-mail já está em uso por outro usuário
const existingUser = await db.query.user.findFirst({
where: and(
@@ -181,6 +245,14 @@ export async function updateEmailAction(
};
}
+ // Verificar se o novo e-mail é diferente do atual
+ if (validated.newEmail.toLowerCase() === session.user.email.toLowerCase()) {
+ return {
+ success: false,
+ error: "O novo e-mail deve ser diferente do atual",
+ };
+ }
+
// Atualizar e-mail
await db
.update(schema.user)
@@ -202,7 +274,7 @@ export async function updateEmailAction(
if (error instanceof z.ZodError) {
return {
success: false,
- error: error.errors[0]?.message || "Dados inválidos",
+ error: error.issues[0]?.message || "Dados inválidos",
};
}
@@ -244,7 +316,7 @@ export async function deleteAccountAction(
if (error instanceof z.ZodError) {
return {
success: false,
- error: error.errors[0]?.message || "Dados inválidos",
+ error: error.issues[0]?.message || "Dados inválidos",
};
}
diff --git a/app/(dashboard)/ajustes/page.tsx b/app/(dashboard)/ajustes/page.tsx
index 0296d65..3502e46 100644
--- a/app/(dashboard)/ajustes/page.tsx
+++ b/app/(dashboard)/ajustes/page.tsx
@@ -5,6 +5,8 @@ import { UpdatePasswordForm } from "@/components/ajustes/update-password-form";
import { Card } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { auth } from "@/lib/auth/config";
+import { db, schema } from "@/lib/db";
+import { eq } from "drizzle-orm";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
@@ -20,6 +22,14 @@ export default async function Page() {
const userName = session.user.name || "";
const userEmail = session.user.email || "";
+ // Detectar método de autenticação (Google OAuth vs E-mail/Senha)
+ const userAccount = await db.query.account.findFirst({
+ where: eq(schema.account.userId, session.user.id),
+ });
+
+ // Se o providerId for "google", o usuário usa Google OAuth
+ const authProvider = userAccount?.providerId || "credential";
+
return (
@@ -51,7 +61,7 @@ export default async function Page() {
Defina uma nova senha para sua conta. Guarde-a em local seguro.
-
+
@@ -63,7 +73,7 @@ export default async function Page() {
atual (quando aplicável) para concluir a alteração.
-
+
diff --git a/components/ajustes/update-email-form.tsx b/components/ajustes/update-email-form.tsx
index ed862f1..d81b2cc 100644
--- a/components/ajustes/update-email-form.tsx
+++ b/components/ajustes/update-email-form.tsx
@@ -4,29 +4,61 @@ import { updateEmailAction } from "@/app/(dashboard)/ajustes/actions";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
-import { useState, useTransition } from "react";
+import { RiCheckLine, RiCloseLine, RiEyeLine, RiEyeOffLine } from "@remixicon/react";
+import { useState, useTransition, useMemo } from "react";
import { toast } from "sonner";
type UpdateEmailFormProps = {
currentEmail: string;
+ authProvider?: string; // 'google' | 'credential' | undefined
};
-export function UpdateEmailForm({ currentEmail }: UpdateEmailFormProps) {
+export function UpdateEmailForm({ currentEmail, authProvider }: UpdateEmailFormProps) {
const [isPending, startTransition] = useTransition();
+ const [password, setPassword] = useState("");
const [newEmail, setNewEmail] = useState("");
const [confirmEmail, setConfirmEmail] = useState("");
+ const [showPassword, setShowPassword] = useState(false);
+
+ // Verificar se o usuário usa login via Google (não precisa de senha)
+ const isGoogleAuth = authProvider === "google";
+
+ // Validação em tempo real: e-mails coincidem
+ const emailsMatch = useMemo(() => {
+ if (!confirmEmail) return null; // Não mostrar erro se campo vazio
+ return newEmail.toLowerCase() === confirmEmail.toLowerCase();
+ }, [newEmail, confirmEmail]);
+
+ // Validação: novo e-mail é diferente do atual
+ const isEmailDifferent = useMemo(() => {
+ if (!newEmail) return true;
+ return newEmail.toLowerCase() !== currentEmail.toLowerCase();
+ }, [newEmail, currentEmail]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
+ // Validação frontend antes de enviar
+ if (newEmail.toLowerCase() !== confirmEmail.toLowerCase()) {
+ toast.error("Os e-mails não coincidem");
+ return;
+ }
+
+ if (newEmail.toLowerCase() === currentEmail.toLowerCase()) {
+ toast.error("O novo e-mail deve ser diferente do atual");
+ return;
+ }
+
startTransition(async () => {
const result = await updateEmailAction({
+ password: isGoogleAuth ? undefined : password,
newEmail,
confirmEmail,
});
if (result.success) {
toast.success(result.message);
+ setPassword("");
setNewEmail("");
setConfirmEmail("");
} else {
@@ -37,33 +69,145 @@ export function UpdateEmailForm({ currentEmail }: UpdateEmailFormProps) {
return (
diff --git a/components/ajustes/update-password-form.tsx b/components/ajustes/update-password-form.tsx
index 0f3d361..6103a68 100644
--- a/components/ajustes/update-password-form.tsx
+++ b/components/ajustes/update-password-form.tsx
@@ -4,28 +4,69 @@ import { updatePasswordAction } from "@/app/(dashboard)/ajustes/actions";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
-import { RiEyeLine, RiEyeOffLine } from "@remixicon/react";
-import { useState, useTransition } from "react";
+import { RiEyeLine, RiEyeOffLine, RiCheckLine, RiCloseLine, RiAlertLine } from "@remixicon/react";
+import { useState, useTransition, useMemo } from "react";
import { toast } from "sonner";
-export function UpdatePasswordForm() {
+type UpdatePasswordFormProps = {
+ authProvider?: string; // 'google' | 'credential' | undefined
+};
+
+export function UpdatePasswordForm({ authProvider }: UpdatePasswordFormProps) {
const [isPending, startTransition] = useTransition();
+ const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
+ const [showCurrentPassword, setShowCurrentPassword] = useState(false);
const [showNewPassword, setShowNewPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
+ // Verificar se o usuário usa login via Google
+ const isGoogleAuth = authProvider === "google";
+
+ // Validação em tempo real: senhas coincidem
+ const passwordsMatch = useMemo(() => {
+ if (!confirmPassword) return null; // Não mostrar erro se campo vazio
+ return newPassword === confirmPassword;
+ }, [newPassword, confirmPassword]);
+
+ // Indicador de força da senha (básico)
+ const passwordStrength = useMemo(() => {
+ if (!newPassword) return null;
+ if (newPassword.length < 6) return "weak";
+ if (newPassword.length >= 12 && /[A-Z]/.test(newPassword) && /[0-9]/.test(newPassword) && /[^A-Za-z0-9]/.test(newPassword)) {
+ return "strong";
+ }
+ if (newPassword.length >= 8 && (/[A-Z]/.test(newPassword) || /[0-9]/.test(newPassword))) {
+ return "medium";
+ }
+ return "weak";
+ }, [newPassword]);
+
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
+ // Validação frontend antes de enviar
+ if (newPassword !== confirmPassword) {
+ toast.error("As senhas não coincidem");
+ return;
+ }
+
+ if (newPassword.length < 6) {
+ toast.error("A senha deve ter no mínimo 6 caracteres");
+ return;
+ }
+
startTransition(async () => {
const result = await updatePasswordAction({
+ currentPassword,
newPassword,
confirmPassword,
});
if (result.success) {
toast.success(result.message);
+ setCurrentPassword("");
setNewPassword("");
setConfirmPassword("");
} else {
@@ -34,10 +75,69 @@ export function UpdatePasswordForm() {
});
};
+ // Se o usuário usa Google OAuth, mostrar aviso
+ if (isGoogleAuth) {
+ return (
+
+
+
+
+
+ Alteração de senha não disponível
+
+
+ Você fez login usando sua conta do Google. A senha é gerenciada diretamente pelo Google
+ e não pode ser alterada aqui. Para modificar sua senha, acesse as configurações de
+ segurança da sua conta Google.
+
+
+
+
+ );
+ }
+
return (