From a4847374fcd42134ba670605b5009a7a3e463a5c Mon Sep 17 00:00:00 2001 From: Stivenjs Date: Fri, 9 May 2025 21:49:38 -0500 Subject: [PATCH] refactor: improve type safety and CI workflow configuration - Replace `any` with `unknown` in userStore.ts and StoreSelector.tsx for better type safety - Update CI workflows to trigger on `main` and `dev` branches only - Enhance `useAuthUser` hook with loading state, error handling, and event-based data refresh - Clean up comments in amplify/data/resource.ts for better readability --- .github/workflows/codeql.yml | 4 +- .github/workflows/unit_test.yml | 3 +- amplify/data/resource.ts | 106 +++++++++--------- .../my-store/components/StoreSelector.tsx | 18 ++- context/core/userStore.ts | 2 +- hooks/auth/useAuthUser.ts | 80 ++++++++++--- 6 files changed, 138 insertions(+), 75 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5eca4d07..a210024e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,9 +13,9 @@ name: "CodeQL Advanced" on: push: - branches: ["*"] + branches: ["main", "dev"] pull_request: - branches: ["*"] + branches: ["main", "dev"] schedule: - cron: '27 8 * * 3' diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index c02685dd..2a0b4bee 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -3,7 +3,8 @@ name: Ejecutar tests con Jest on: push: branches: - - '*' # En cualquier push a cualquier rama + - main + - dev pull_request: branches: - main diff --git a/amplify/data/resource.ts b/amplify/data/resource.ts index 70aeac2d..1ce7cdfd 100644 --- a/amplify/data/resource.ts +++ b/amplify/data/resource.ts @@ -72,14 +72,14 @@ const schema = a UserSubscription: a .model({ id: a.id().required(), - userId: a.string().required(), // Llave primaria (external_reference) - subscriptionId: a.string().required(), // Id de la suscripción - planName: a.string().required(), // Nombre del plan (reason) - nextPaymentDate: a.datetime(), // Próxima fecha de pago (opcional) - pendingPlan: a.string(), // Nuevo plan pendiente (opcional) - pendingStartDate: a.datetime(), // Fecha del plan pendiente a activar - planPrice: a.float(), // Precio del plan - lastFourDigits: a.integer(), // Últimos 4 dígitos de la tarjeta + userId: a.string().required(), + subscriptionId: a.string().required(), + planName: a.string().required(), + nextPaymentDate: a.datetime(), + pendingPlan: a.string(), + pendingStartDate: a.datetime(), + planPrice: a.float(), + lastFourDigits: a.integer(), }) .identifier(['id']) .authorization(allow => [ @@ -89,19 +89,19 @@ const schema = a UserStore: a .model({ - userId: a.string().required(), // Relaciona la tienda con el usuario - storeId: a.string().required(), // Identificador único de la tienda - storeName: a.string().required(), // Nombre de la tienda - storeDescription: a.string(), // Descripción opcional de la tienda - storeLogo: a.string(), // URL de la imagen del logo de la tienda - storeFavicon: a.string(), // URL de la imagen del favicon de la tienda - storeBanner: a.string(), // URL de la imagen del banner de la tienda - storeTheme: a.string(), // Tema de la tienda (opcional) - storeCurrency: a.string(), // Moneda de la tienda - storeType: a.string(), // Tipo de tienda (física, virtual, etc.) - storeStatus: a.string(), // Estado de la tienda (activa, inactiva, etc.) - storePolicy: a.string(), // Política de la tienda (opcional) - storeAdress: a.string(), // Dirección de la tienda + userId: a.string().required(), + storeId: a.string().required(), + storeName: a.string().required(), + storeDescription: a.string(), + storeLogo: a.string(), + storeFavicon: a.string(), + storeBanner: a.string(), + storeTheme: a.string(), + storeCurrency: a.string(), + storeType: a.string(), + storeStatus: a.string(), + storePolicy: a.string(), + storeAdress: a.string(), contactEmail: a.string(), contactPhone: a.float(), contactName: a.string(), @@ -120,49 +120,49 @@ const schema = a Product: a .model({ id: a.id().required(), - storeId: a.string().required(), // Relaciona el producto con la tienda - name: a.string().required(), // Nombre del producto - description: a.string(), // Descripción del producto - price: a.float(), // Precio del producto - compareAtPrice: a.float(), // Precio de comparación (opcional) - costPerItem: a.float(), // Costo por artículo (opcional) - sku: a.string(), // SKU del producto (opcional) - barcode: a.string(), // Código de barras (opcional) - quantity: a.integer(), // Cantidad en inventario - category: a.string(), // Categoría del producto - images: a.json(), // Array de imágenes [{url: string, alt: string}] - attributes: a.json(), // Array de atributos [{name: string, values: string[]}] - status: a.string(), // Estado: ACTIVE, INACTIVE, PENDING, DRAFT - slug: a.string(), // URL amigable del producto - featured: a.boolean(), // Producto destacado - tags: a.json(), // Array de etiquetas - variants: a.json(), // Variantes del producto - collectionId: a.string(), // ID de la colección - supplier: a.string(), // Proveedor del producto - collection: a.belongsTo('Collection', 'collectionId'), // Relación con la colección - owner: a.string().required(), // Usuario que creo el producto + storeId: a.string().required(), + name: a.string().required(), + description: a.string(), + price: a.float(), + compareAtPrice: a.float(), + costPerItem: a.float(), + sku: a.string(), + barcode: a.string(), + quantity: a.integer(), + category: a.string(), + images: a.json(), + attributes: a.json(), + status: a.string(), + slug: a.string(), + featured: a.boolean(), + tags: a.json(), + variants: a.json(), + collectionId: a.string(), + supplier: a.string(), + collection: a.belongsTo('Collection', 'collectionId'), + owner: a.string().required(), }) .authorization(allow => [ - allow.ownerDefinedIn('owner').to(['update', 'delete', 'read', 'create']), // Solo el creador puede editar y eliminar + allow.ownerDefinedIn('owner').to(['update', 'delete', 'read', 'create']), allow.guest().to(['read']), ]), Collection: a .model({ - storeId: a.string().required(), // Relaciona la colección con la tienda - title: a.string().required(), // Nombre de la colección - description: a.string(), // Descripción de la colección - image: a.string(), // URL de la imagen de la colección - slug: a.string(), // URL amigable de la colección + storeId: a.string().required(), + title: a.string().required(), + description: a.string(), + image: a.string(), + slug: a.string(), isActive: a.boolean().required(), - sortOrder: a.integer(), // Orden de la colección - owner: a.string().required(), // Usuario que creo la colección - products: a.hasMany('Product', 'collectionId'), // Relación con productos + sortOrder: a.integer(), + owner: a.string().required(), + products: a.hasMany('Product', 'collectionId'), }) .secondaryIndexes(index => [index('storeId')]) .authorization(allow => [ - allow.ownerDefinedIn('owner').to(['update', 'delete', 'read', 'create']), // Solo el creador puede editar y eliminar - allow.guest().to(['read']), // Visitantes pueden ver las colecciones + allow.ownerDefinedIn('owner').to(['update', 'delete', 'read', 'create']), + allow.guest().to(['read']), ]), }) .authorization(allow => [ diff --git a/app/(setup-layout)/my-store/components/StoreSelector.tsx b/app/(setup-layout)/my-store/components/StoreSelector.tsx index 2e14da44..9a3015e7 100644 --- a/app/(setup-layout)/my-store/components/StoreSelector.tsx +++ b/app/(setup-layout)/my-store/components/StoreSelector.tsx @@ -1,3 +1,5 @@ +'use client' + import Image from 'next/image' import Link from 'next/link' import { Suspense } from 'react' @@ -107,7 +109,7 @@ function StoreData({ userId, userPlan }: { userId: string | null; userPlan?: str stores = [], canCreateStore = false, error, - } = result as { stores: any[]; canCreateStore: boolean; error?: string } + } = result as { stores: unknown[]; canCreateStore: boolean; error?: string } if (error) { return ( @@ -120,10 +122,22 @@ function StoreData({ userId, userPlan }: { userId: string | null; userPlan?: str // Componente principal export function StoreSelector() { - const { userData } = useAuthUser() + const { userData, isLoading } = useAuthUser() const cognitoUsername = userData?.['cognito:username'] const userPlan = userData?.['custom:plan'] + if (isLoading) { + return ( + + ) + } + return ( { - // Estado para guardar el payload completo - const [userData, setUserData] = useState(null) +interface UserPayload { + sub: string + email: string + nickName?: string + phone?: string + cognitoUsername: string + userId: string + plan?: string + picture?: string + identities?: unknown[] + [key: string]: any +} - useEffect(() => { - // Función para obtener la sesión y los datos del usuario - const fetchUserData = async () => { - try { - const session = await fetchAuthSession() - const payload = session.tokens?.idToken?.payload - setUserData(payload) - } catch (error) { - console.error('Error fetching user data:', error) - } +interface AuthUserResult { + userData: UserPayload | null + isLoading: boolean + error: Error | null + refreshUserData: () => Promise +} + +export const useAuthUser = (): AuthUserResult => { + const [userData, setUserData] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + const fetchUserData = useCallback(async () => { + setIsLoading(true) + setError(null) + + try { + const session = await fetchAuthSession() + const payload = session.tokens?.idToken?.payload as UserPayload + setUserData(payload) + } catch (err) { + console.error('Error fetching user data:', err) + setError(err instanceof Error ? err : new Error('Error desconocido')) + setUserData(null) + } finally { + setIsLoading(false) } + }, []) + // Escucha eventos de Auth para refrescar los datos + useEffect(() => { fetchUserData() - }, []) - return { userData } + const unsubscribe = Hub.listen('auth', ({ payload }) => { + if ( + ['signIn', 'signOut', 'tokenRefresh', 'signIn_failure', 'signOut_failure'].includes( + payload.event + ) + ) { + fetchUserData() + } + }) + + return () => { + unsubscribe() // limpia el listener al desmontar + } + }, [fetchUserData]) + + return { + userData, + isLoading, + error, + refreshUserData: fetchUserData, + } }