From 0420fd1697989c7610a22d4421051aede5cfd9b6 Mon Sep 17 00:00:00 2001 From: Stivenjs Date: Mon, 3 Feb 2025 18:59:09 -0500 Subject: [PATCH 001/192] =?UTF-8?q?Agregar=20nuevos=20campos=20de=20usuari?= =?UTF-8?q?o=20(bio=20y=20tel=C3=A9fono),=20actualizar=20URLs=20de=20auten?= =?UTF-8?q?ticaci=C3=B3n=20y=20mejorar=20el=20dise=C3=B1o=20de=20component?= =?UTF-8?q?es=20en=20la=20p=C3=A1gina=20de=20inicio=20de=20sesi=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- amplify/auth/resource.ts | 25 +- amplify/backend.ts | 4 +- amplify/functions/webHookPlan/src/handler.ts | 6 +- amplify/storage/resource.ts | 15 ++ .../components/AccountSettings.tsx | 101 ++++++-- .../components/ChangeEmailDialog.tsx | 123 +++++++++ .../components/ChangePasswordDialog.tsx | 194 ++++++++++++++ .../components/EditProfileDialog.tsx | 242 +++++++++--------- .../components/UserAvatar.tsx | 69 +++++ .../hooks/usePasswordManagement.ts | 120 +++++++++ .../hooks/useUpdateProfilePicture.ts | 53 ++++ .../hooks/useUserAttributes.ts | 109 ++++++++ app/account-settings/page.tsx | 5 +- app/landing/components/Footer.tsx | 10 +- app/landing/components/NavBar.tsx | 9 +- app/login/AuthForm.tsx | 23 +- app/login/components/AuthClient.tsx | 2 +- app/login/components/sing-in/SignInForm.tsx | 13 +- app/login/components/sing-up/SignUpForm.tsx | 20 +- .../verification-form/VerificationForm.tsx | 10 +- app/login/hooks/SignIn.ts | 48 +++- app/login/hooks/signUp.ts | 5 +- app/pricing/page.tsx | 8 +- app/terms/components/LegalDocuments.tsx | 102 ++++++++ app/terms/components/legal-content.json | 140 ++++++++++ app/terms/page.tsx | 19 ++ components/ui/scroll-area.tsx | 46 ++++ hooks/auth/useAuth.ts | 4 +- hooks/custom-toast/use-toast.ts | 2 +- lib/schemas.ts | 6 +- package-lock.json | 142 ++++++++++ package.json | 1 + store/userStore.ts | 2 + 33 files changed, 1482 insertions(+), 196 deletions(-) create mode 100644 amplify/storage/resource.ts create mode 100644 app/account-settings/components/ChangeEmailDialog.tsx create mode 100644 app/account-settings/components/ChangePasswordDialog.tsx create mode 100644 app/account-settings/components/UserAvatar.tsx create mode 100644 app/account-settings/hooks/usePasswordManagement.ts create mode 100644 app/account-settings/hooks/useUpdateProfilePicture.ts create mode 100644 app/account-settings/hooks/useUserAttributes.ts create mode 100644 app/terms/components/LegalDocuments.tsx create mode 100644 app/terms/components/legal-content.json create mode 100644 app/terms/page.tsx create mode 100644 components/ui/scroll-area.tsx diff --git a/amplify/auth/resource.ts b/amplify/auth/resource.ts index e5a8a652..102b1927 100644 --- a/amplify/auth/resource.ts +++ b/amplify/auth/resource.ts @@ -29,16 +29,16 @@ export const auth = defineAuth({ }, }, - callbackUrls: [ - "https://feature-get-started.d705ckpcaa3mv.amplifyapp.com", - ], - logoutUrls: [ - "https://feature-get-started.d705ckpcaa3mv.amplifyapp.com/login", - ], + callbackUrls: ["http://localhost:3000"], + logoutUrls: ["http://localhost:3000/login"], }, }, userAttributes: { + nickname: { + mutable: true, + required: false, + }, preferredUsername: { mutable: true, required: false, @@ -49,6 +49,19 @@ export const auth = defineAuth({ maxLen: 255, minLen: 1, }, + + "custom:bio": { + mutable: true, + dataType: "String", + maxLen: 255, + minLen: 1, + }, + "custom:phone": { + mutable: true, + dataType: "String", + maxLen: 255, + minLen: 1, + }, }, access: (allow) => [ diff --git a/amplify/backend.ts b/amplify/backend.ts index a3c2c4e7..13d44b4b 100644 --- a/amplify/backend.ts +++ b/amplify/backend.ts @@ -1,6 +1,7 @@ import { defineBackend } from "@aws-amplify/backend"; import { auth } from "./auth/resource"; import { data } from "./data/resource"; +import { storage } from "./storage/resource"; import { createSubscription } from "./functions/createSubscription/resource"; import { webHookPlan } from "./functions/webHookPlan/resource"; import { Stack } from "aws-cdk-lib"; @@ -18,6 +19,7 @@ import { Policy, PolicyStatement } from "aws-cdk-lib/aws-iam"; const backend = defineBackend({ auth, data, + storage, createSubscription, webHookPlan, }); @@ -64,8 +66,6 @@ const webhookPath = myRestApi.root.addResource("webhook", { }, }); - - // Agregar el método POST para el webhook webhookPath.addMethod("POST", webHookPlanIntegration); diff --git a/amplify/functions/webHookPlan/src/handler.ts b/amplify/functions/webHookPlan/src/handler.ts index 497ebf34..b545a2f2 100644 --- a/amplify/functions/webHookPlan/src/handler.ts +++ b/amplify/functions/webHookPlan/src/handler.ts @@ -12,7 +12,7 @@ const client = new CognitoIdentityProviderClient(); // Clave secreta de Mercado Pago (debes configurarla en el panel de Mercado Pago) const MERCADO_PAGO_WEBHOOK_SECRET = - "086cb13be04912968067956f4b3f887508fa64c6b1177955e1c7f10aaebd098b"; + "1384e0f904220759e2d1f2ed68c4c00877bb642389684614677a1372b6ee5347"; // Token de acceso de Mercado Pago const MERCADO_PAGO_ACCESS_TOKEN = @@ -98,7 +98,7 @@ export const handler: APIGatewayProxyHandler = async (event) => { // 8. Obtener el plan actual del usuario desde Cognito const getUserCommand = new AdminGetUserCommand({ - UserPoolId: "us-east-2_4yZwbrdZc", + UserPoolId: "us-east-2_EVU1jxAq4", Username: userId, }); @@ -143,7 +143,7 @@ export const handler: APIGatewayProxyHandler = async (event) => { // 11. Actualizar el atributo personalizado en Cognito (solo si es necesario) if (status !== "pending" && planValue !== currentPlan) { const command = new AdminUpdateUserAttributesCommand({ - UserPoolId: "us-east-2_4yZwbrdZc", + UserPoolId: "us-east-2_EVU1jxAq4", Username: userId, UserAttributes: [ { diff --git a/amplify/storage/resource.ts b/amplify/storage/resource.ts new file mode 100644 index 00000000..8422563e --- /dev/null +++ b/amplify/storage/resource.ts @@ -0,0 +1,15 @@ +import { defineStorage } from "@aws-amplify/backend"; + +export const storage = defineStorage({ + name: "amplifyTeamDrive", + access: (allow) => ({ + "profile-pictures/{entity_id}/*": [ + allow.guest.to(["read"]), // Permite a los invitados leer + allow.entity("identity").to(["read", "write", "delete"]), // Permite al propietario leer, escribir y eliminar + ], + "picture-submissions/*": [ + allow.authenticated.to(["read", "write"]), // Permite a los usuarios autenticados leer y escribir + allow.guest.to(["read", "write"]), // Permite a los invitados leer y escribir + ], + }), +}); diff --git a/app/account-settings/components/AccountSettings.tsx b/app/account-settings/components/AccountSettings.tsx index 3e49d245..141834f9 100644 --- a/app/account-settings/components/AccountSettings.tsx +++ b/app/account-settings/components/AccountSettings.tsx @@ -2,7 +2,6 @@ import { useState } from "react"; import { Pencil, BadgeCheck, LogOut } from "lucide-react"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { EditProfileDialog } from "@/app/account-settings/components/EditProfileDialog"; import { @@ -19,7 +18,12 @@ import { Amplify } from "aws-amplify"; import { useAuthUser } from "@/hooks/auth/useAuthUser"; import { deleteUser } from "aws-amplify/auth"; import { useRouter } from "next/navigation"; +import { useAuth } from "@/hooks/auth/useAuth"; +import { LoadingIndicator } from "@/components/ui/loading-indicator"; +import { UserAvatar } from "@/app/account-settings/components/UserAvatar"; +import { ChangePasswordDialog } from "@/app/account-settings/components/ChangePasswordDialog"; import useUserStore from "@/store/userStore"; +import { ChangeEmailDialog } from "@/app/account-settings/components/ChangeEmailDialog"; import outputs from "@/amplify_outputs.json"; Amplify.configure(outputs); @@ -27,7 +31,10 @@ Amplify.configure(outputs); export function AccountSettings() { const [isProfileOpen, setIsProfileOpen] = useState(false); const [isDeleteAccountOpen, setIsDeleteAccountOpen] = useState(false); + const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false); + const [isChangeEmailOpen, setIsChangeEmailOpen] = useState(false); const { user } = useUserStore(); + const { loading } = useAuth(); const { userData } = useAuthUser(); const router = useRouter(); @@ -42,7 +49,7 @@ export function AccountSettings() { } // Obtén el nombre completo del usuario - const fullName = user?.nickName || user?.preferredUsername; + const fullName = user?.nickName; // Separa el nombre en partes const nameParts = fullName ? fullName.split(" ") : []; @@ -52,27 +59,23 @@ export function AccountSettings() { // Verifica si el usuario ha iniciado sesión con Google const isGoogleUser = userData?.identities; + if (loading) { + return ; + } + return (

Mi Perfil

- - - - {firstName.charAt(0)} - {lastName.charAt(0)} - - +

{fullName}

Plan activo: {user?.plan}

-
-

- Correo electrónico verificado -

- -
-
- Correo electrónico -
-
{user?.email}
+
Teléfono
+
{user?.phone || "No especificado"}
-
Teléfono
-
No especificado
+
Bio
+
{user?.bio || "No especificado"}
@@ -164,6 +165,54 @@ export function AccountSettings() {
)} +
+

Correo Electrónico

+
+
+
+
+

{user?.email}

+
+

+ Correo electrónico verificado +

+ +
+
+
+ +
+
+
+
+

Seguridad

+
+
+
+

Contraseña

+

+ Cambia tu contraseña regularmente para mantener tu cuenta segura +

+
+ +
+
+
+

Zona de Peligro

@@ -180,7 +229,10 @@ export function AccountSettings() {

- + + ); } diff --git a/app/account-settings/components/ChangeEmailDialog.tsx b/app/account-settings/components/ChangeEmailDialog.tsx new file mode 100644 index 00000000..502d7e17 --- /dev/null +++ b/app/account-settings/components/ChangeEmailDialog.tsx @@ -0,0 +1,123 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { toast } from "@/hooks/use-toast"; +import { useUserAttributes } from "@/app/account-settings/hooks/useUserAttributes"; + +interface ChangeEmailDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + currentEmail: string; +} + +export function ChangeEmailDialog({ + open, + onOpenChange, + currentEmail, +}: ChangeEmailDialogProps) { + const [newEmail, setNewEmail] = useState(""); + const [verificationCode, setVerificationCode] = useState(""); + const [requiresVerification, setRequiresVerification] = useState(false); + const { updateAttributes, confirmAttribute, loading, error } = + useUserAttributes(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + console.log("Formulario enviado"); + + try { + if (!requiresVerification) { + console.log("Intentando actualizar el correo electrónico..."); + const updateResult = await updateAttributes({ email: newEmail }); + console.log("Resultado de la actualización:", updateResult); + + if ( + updateResult.nextStep.nextStep.updateAttributeStep === + "CONFIRM_ATTRIBUTE_WITH_CODE" + ) { + setRequiresVerification(true); + toast({ + title: "Código de verificación enviado", + description: `Se ha enviado un código de verificación a ${newEmail}. Por favor, revisa tu nuevo correo electrónico.`, + }); + } + } else { + console.log("Intentando confirmar el atributo..."); + await confirmAttribute({ + userAttributeKey: "email", + confirmationCode: verificationCode, + }); + console.log("Correo electrónico confirmado y actualizado"); + + toast({ + title: "Correo electrónico actualizado", + description: + "Tu correo electrónico ha sido actualizado exitosamente.", + }); + onOpenChange(false); + } + } catch (err) { + console.error("Error detallado:", err); + let errorMessage = + "Hubo un problema al procesar tu solicitud. Por favor, inténtalo de nuevo."; + if (err instanceof Error) { + errorMessage = err.message; + } + toast({ + title: "Error", + description: errorMessage, + variant: "destructive", + }); + } + }; + + return ( + + + + Cambiar correo electrónico + + {requiresVerification + ? `Introduce el código de verificación enviado a ${newEmail}.` + : "Introduce tu nuevo correo electrónico."} + + +
+ {!requiresVerification && ( + setNewEmail(e.target.value)} + /> + )} + {requiresVerification && ( + setVerificationCode(e.target.value)} + /> + )} + + + +
+
+
+ ); +} diff --git a/app/account-settings/components/ChangePasswordDialog.tsx b/app/account-settings/components/ChangePasswordDialog.tsx new file mode 100644 index 00000000..0f2252f3 --- /dev/null +++ b/app/account-settings/components/ChangePasswordDialog.tsx @@ -0,0 +1,194 @@ +"use client"; + +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import usePasswordManagement from "@/app/account-settings/hooks/usePasswordManagement"; +import { Eye, EyeOff, Loader2 } from "lucide-react"; + +const passwordSchema = z + .object({ + oldPassword: z.string().min(1, "La contraseña actual es requerida"), + newPassword: z + .string() + .min(8, "La nueva contraseña debe tener al menos 8 caracteres") + .regex( + /[!@#$%^&*()\-_=+{};:,<.>]/, + "La contraseña debe contener al menos una letra mayúscula, una minúscula, un número y un carácter especial" + ), + confirmPassword: z.string().min(1, "Confirma tu nueva contraseña"), + }) + .refine((data) => data.newPassword === data.confirmPassword, { + message: "Las contraseñas no coinciden", + path: ["confirmPassword"], + }); + +type PasswordFormValues = z.infer; + +export function ChangePasswordDialog({ + open, + onOpenChange, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; +}) { + const { + updateUserPassword, + loading, + error: hookError, + success, + } = usePasswordManagement(); + + const [showOldPassword, setShowOldPassword] = useState(false); + const [showNewPassword, setShowNewPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + + const form = useForm({ + resolver: zodResolver(passwordSchema), + defaultValues: { + oldPassword: "", + newPassword: "", + confirmPassword: "", + }, + }); + + const onSubmit = async (values: PasswordFormValues) => { + try { + await updateUserPassword(values.oldPassword, values.newPassword); + form.reset(); + if (success) { + setTimeout(() => onOpenChange(false), 2000); + } + } catch (err) { + console.error(err); + } + }; + + const PasswordInput = ({ field, show, setShow, placeholder }: any) => ( +
+ + +
+ ); + + return ( + + + + Cambiar Contraseña + +
+ + ( + + Contraseña actual + + + + + + )} + /> + ( + + Nueva contraseña + + + + + + )} + /> + ( + + Confirmar nueva contraseña + + + + + + )} + /> + {hookError && ( +

+ Error al cambiar la contraseña. Por favor, inténtalo de nuevo. +

+ )} + {success && ( +

+ Contraseña cambiada exitosamente. +

+ )} + + + +
+
+ ); +} diff --git a/app/account-settings/components/EditProfileDialog.tsx b/app/account-settings/components/EditProfileDialog.tsx index b2254769..4488cd76 100644 --- a/app/account-settings/components/EditProfileDialog.tsx +++ b/app/account-settings/components/EditProfileDialog.tsx @@ -20,17 +20,17 @@ import { import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; -import { useToast } from "@/hooks/use-toast"; import { useEffect } from "react"; +import { useUserAttributes } from "@/app/account-settings/hooks/useUserAttributes"; +import { Loader2 } from "lucide-react"; +import { useToast } from "@/hooks/custom-toast/use-toast"; +import { Toast } from "@/components/ui/toasts"; import useAuthStore from "@/store/userStore"; - - const formSchema = z.object({ - firstName: z.string().min(2, "First name must be at least 2 characters"), - lastName: z.string().min(2, "Last name must be at least 2 characters"), - email: z.string().email("Invalid email address"), - phone: z.string().min(10, "Phone number must be at least 10 characters"), + firstName: z.string().min(2, "El nombre debe tener al menos 2 caracteres"), + lastName: z.string().min(2, "El apellido debe tener al menos 2 caracteres"), + phone: z.string().min(10, "El teléfono debe tener al menos 10 caracteres"), bio: z.string(), }); @@ -43,133 +43,145 @@ export function EditProfileDialog({ open, onOpenChange, }: EditProfileDialogProps) { - const { toast } = useToast(); + const { toasts, addToast, removeToast } = useToast(); const { user } = useAuthStore(); + const { updateAttributes, loading, error, nextStep } = useUserAttributes(); - const fullName = user?.nickName || user?.preferredUsername; - + const fullName = user?.nickName; const nameParts = fullName ? fullName.split(" ") : []; const firstName = nameParts[0] || ""; const lastName = nameParts[nameParts.length - 1] || ""; const form = useForm>({ resolver: zodResolver(formSchema), - defaultValues: { - firstName: "", - lastName: "", - email: "", - phone: "+44 123 456 789", - bio: "Team Manager", - }, }); - // Usa useEffect para actualizar los valores del formulario cuando `user` cambie useEffect(() => { if (user) { form.reset({ - firstName: firstName, - lastName: lastName, - email: user?.email, - phone: "+44 123 456 789", - bio: "Team Manager", + firstName, + lastName, + phone: user.phone || "Sin especificar", + bio: user.bio || "Sin especificar", }); } }, [user, firstName, lastName, form]); - function onSubmit(values: z.infer) { - toast({ - title: "Profile updated", - description: "Your profile has been updated successfully.", - }); - onOpenChange(false); + async function onSubmit(values: z.infer) { + try { + const nickname = `${values.firstName} ${values.lastName}`; + // Mapea los campos del formulario a los atributos que deseas actualizar. + await updateAttributes({ + nickname: nickname, + "custom:phone": values.phone, + "custom:bio": values.bio, + }); + + // Si el siguiente paso es la confirmación, podrías mostrar otra interfaz para que el usuario ingrese el código. + if (nextStep === "CONFIRM_ATTRIBUTE_WITH_CODE") { + alert("Se necesita un codigo de confirmacion"); + // Aquí podrías abrir un diálogo/modal para que el usuario ingrese el código + } else { + addToast("Tu perfil fue actualizado exitosamente!", "success"); + onOpenChange(false); + } + } catch (err) { + console.error("Error al actualizar atributos:", err); + addToast("Ocurrió un error al actualizar el perfil.", "error"); + } } return ( - - - - Editar perfil - -
- - ( - - Nombre - - - - - - )} - /> - ( - - Apellido - - - - - - )} - /> - ( - - Email - - - - - - )} - /> - ( - - Phone - - - - - - )} - /> - ( - - Bio - -