Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Build
on:
push:
branches:
- main
- dev
pull_request:
types: [opened, synchronize, reopened]
jobs:
sonarqube:
name: SonarQube
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@v5
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
70 changes: 57 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Fasttify - Dropshipping Ecommerce Platform

Welcome to **Fasttify**, the ultimate SaaS solution for creating and managing personalized dropshipping stores effortlessly. Built on **AWS Amplify** with a modern **Next.js** front end, Fasttify combines scalability, performance, and a user-friendly interface.
Welcome to **Fasttify**, the ultimate SaaS solution for creating and managing personalized dropshipping stores effortlessly. Built on **AWS Amplify Gen2** with a modern **Next.js** front end, Fasttify combines scalability, performance, and a user-friendly interface.

## Overview

Expand All @@ -12,21 +12,20 @@ Fasttify empowers users to build their ecommerce business with:

## Features

- **Authentication**: Secure and customizable user sign-up and sign-in powered by AWS Cognito.
- **Authentication**: Secure and customizable user sign-up and sign-in powered by AWS Cognito with Amplify Gen2's improved TypeScript support.
- **Subscriptions**: Integrated with **Mercado Pago**, enabling easy subscription management with upgrade, downgrade, and cancellation functionality.
- **Custom Plans**: Personalize user plans with AWS Lambda and custom attributes.
- **API and Database**: Leverages AWS AppSync (GraphQL API) and DynamoDB for fast, scalable data management.
- **Custom Plans**: Personalize user plans with AWS Lambda and custom attributes, leveraging Gen2's enhanced type safety.
- **API and Database**: Utilizes Amplify Gen2's improved data modeling with TypeScript for AWS AppSync (GraphQL API) and DynamoDB, providing fast, scalable data management.
- **Webhooks**: Stay synchronized with real-time notifications for subscription updates.
- **Type Safety**: Benefit from Amplify Gen2's TypeScript-first approach for better developer experience and fewer runtime errors.
- **Local Development**: Enhanced local development experience with Gen2's improved tooling and emulators.

## Quick Start

1. **Clone the Repository**:

```bash

git clone https://github.com/Stivenjs/Fasttify.git


cd fasttify
```

Expand All @@ -36,19 +35,23 @@ Fasttify empowers users to build their ecommerce business with:
npm install
```

3. **Setup Amplify**:
3. **Setup Amplify Gen2**:

- Initialize Amplify in your project:
```bash
amplify init
npx @aws-amplify/cli@latest init
```
- Add authentication:
```bash
amplify add auth
npx @aws-amplify/cli@latest add auth
```
- Generate the TypeScript definitions:
```bash
npx @aws-amplify/cli@latest generate
```
- Push the changes:
- Deploy your backend:
```bash
amplify push
npx @aws-amplify/cli@latest deploy
```

4. **Start the Development Server**:
Expand All @@ -59,6 +62,47 @@ Fasttify empowers users to build their ecommerce business with:

Your app will be live at `http://localhost:3000`.

## Amplify Gen2 Benefits

Fasttify leverages AWS Amplify Gen2 to provide:

- **TypeScript-First Experience**: Improved type safety and developer experience.
- **Simplified Resource Definition**: Define your backend resources using TypeScript.
- **Enhanced Local Development**: Test your app locally with improved emulators.
- **Flexible Deployment Options**: Deploy your entire stack or individual resources.
- **Better Performance**: Optimized client libraries for faster application performance.
- **Improved DX**: Better error messages and development workflows.

## AWS Account Setup for Local Development

### Prerequisites

If you already have an AWS account and a locally configured profile, you only need to add the IAM role `AmplifyBackendDeployFullAccess` to your configured AWS profile.

### IAM Identity Center Configuration

If you don't have a configured AWS profile, follow these steps:

1. **Enable IAM Identity Center**:

- Sign in to the AWS console
- Access the IAM Identity Center page and select "Enable"
- When prompted, select "Enable with AWS Organizations" and click "Continue"

2. **Configure a user with Amplify permissions**:

- Open CloudShell from the AWS console
- Run commands to set up appropriate permissions

3. **Create a permission set for Amplify**:
- In the IAM Identity Center navigation, select "Permission sets"
- Select "Create permission set"
- Choose "Custom permission set" and click "Next"
- Expand "AWS Managed Policies" and search for "amplify"
- Select "AmplifyBackendDeployFullAccess" and click "Next"
- Name the permission set "amplify-policy" and click "Next"
- Review and select "Create"

## Deploying to AWS

To deploy Fasttify to AWS:
Expand All @@ -67,7 +111,7 @@ To deploy Fasttify to AWS:
2. Set up branches for production and development.
3. Deploy directly from Amplify Console.

Refer to the [AWS Amplify Deployment Guide](https://docs.amplify.aws/nextjs/start/quickstart/nextjs-app-router-client-components/#deploy-a-fullstack-app-to-aws) for detailed instructions.
Refer to the [AWS Amplify Gen2 Deployment Guide](https://docs.amplify.aws/gen2/deploy/fullstack-app/) for detailed instructions.

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion amplify/.config/project-config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"whyContinueWithGen1": "Prefer not to answer",
"projectName": "MasterDop",
"projectName": "fasttify",
"version": "3.1",
"frontend": "javascript",
"javascript": {
Expand Down
1 change: 1 addition & 0 deletions amplify/auth/post-confirmation/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const handler: PostConfirmationTriggerHandler = async event => {
// Crear registro en la tabla UserSubscription
try {
await client.models.UserSubscription.create({
id: event.userName,
userId: event.userName,
subscriptionId: `trial-${event.userName}-${Date.now()}`, // ID único para la suscripción de prueba
planName: 'Royal',
Expand Down
2 changes: 2 additions & 0 deletions amplify/data/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ 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)
Expand All @@ -80,6 +81,7 @@ const schema = a
planPrice: a.float(), // Precio del plan
lastFourDigits: a.integer(), // Últimos 4 dígitos de la tarjeta
})
.identifier(['id'])
.authorization(allow => [
allow.ownerDefinedIn('userId').to(['read', 'update', 'delete']),
allow.authenticated().to(['create']),
Expand Down
39 changes: 30 additions & 9 deletions amplify/functions/planScheduler/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const handler: EventBridgeHandler<'Scheduled Event', null, void> = async
const now = new Date()

// 2. Consultar DynamoDB para obtener las suscripciones pendientes con un plan asignado
// Modificado para solo procesar suscripciones que realmente han expirado o tienen un cambio de plan programado
const pendingSubscriptionsResponse = await clientSchema.models.UserSubscription.list({
filter: {
pendingPlan: { attributeExists: true },
Expand All @@ -29,24 +30,47 @@ export const handler: EventBridgeHandler<'Scheduled Event', null, void> = async
})

const pendingSubscriptions = pendingSubscriptionsResponse.data || []
console.log(`Found ${pendingSubscriptions.length} subscriptions to process`)

// 3. Iterar sobre cada registro pendiente
for (const subscription of pendingSubscriptions) {
const userId = subscription.userId
if (!userId) {
console.warn('⚠️ Suscripción sin userId, omitiendo...')
console.warn('Subscription without userId, skipping')
continue
}

// Leer el valor del plan pendiente desde el registro
const newPlan = subscription.pendingPlan
if (!newPlan) {
console.warn(
`⚠️ La suscripción de ${userId} no tiene un plan pendiente válido, omitiendo...`
console.warn(`The subscription for ${userId} does not have a valid pending plan, skipping`)
continue
}

// Verificar si la suscripción realmente ha expirado o es un cambio de plan programado
const nextPaymentDate = subscription.nextPaymentDate
? new Date(subscription.nextPaymentDate)
: null
const pendingStartDate = subscription.pendingStartDate
? new Date(subscription.pendingStartDate)
: null

// Solo procesar si:
// 1. No hay fecha de próximo pago (suscripción expirada)
// 2. La fecha de próximo pago ya pasó (suscripción expirada)
// 3. La fecha de inicio pendiente está definida y ya pasó (cambio de plan programado)
const shouldProcess =
!nextPaymentDate || nextPaymentDate <= now || (pendingStartDate && pendingStartDate <= now)

if (!shouldProcess) {
console.log(
`Skipping subscription for ${userId}: not expired yet and no pending plan change due`
)
continue
}

console.log(`Processing subscription for ${userId}: changing plan to ${newPlan}`)

try {
// 3.1. Actualizar el atributo en Cognito para asignar el plan pendiente
const updateCommand = new AdminUpdateUserAttributesCommand({
Expand All @@ -56,7 +80,7 @@ export const handler: EventBridgeHandler<'Scheduled Event', null, void> = async
})
await cognitoClient.send(updateCommand)
} catch (cognitoError) {
console.error(`Error actualizando usuario ${userId} en Cognito:`, cognitoError)
console.error(`Error updating user ${userId} in Cognito:`, cognitoError)
continue
}

Expand All @@ -73,13 +97,10 @@ export const handler: EventBridgeHandler<'Scheduled Event', null, void> = async
lastFourDigits: null,
})
} catch (dbError) {
console.error(
`❌ Error actualizando suscripción de usuario ${userId} en DynamoDB:`,
dbError
)
console.error(`Error updating user subscription ${userId} in DynamoDB:`, dbError)
}
}
} catch (error) {
console.error('Error en la Lambda programada:', error)
console.error('Error in scheduled Lambda:', error)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from 'react'
import { Pencil, BadgeCheck, LogOut } from 'lucide-react'
import { Pencil, BadgeCheck } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { EditProfileDialog } from '@/app/(with-navbar)/account-settings/components/EditProfileDialog'
import {
Expand Down Expand Up @@ -158,9 +158,6 @@ export function AccountSettings() {
</p>
</div>
</div>
<Button variant="outline" size="sm" className="gap-2">
<LogOut className="h-4 w-4" /> Desconectar
</Button>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function CancellationDialog({
await onCancel()
setIsCancelled(true)
} catch (error) {
console.error('Error al cancelar la suscripción:', error)
console.error('Error unsubscribing:', error)
} finally {
setIsSubmitting(false)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { Loader2 } from 'lucide-react'
import { Loader } from '@/components/ui/loader'
import {
Dialog,
DialogContent,
Expand Down Expand Up @@ -44,7 +44,6 @@ export function ChangeEmailDialog({ open, onOpenChange, currentEmail }: ChangeEm
})

const {
register: registerVerification,
handleSubmit: handleSubmitVerification,
formState: { errors: verificationErrors },
reset: resetVerification,
Expand Down Expand Up @@ -149,7 +148,7 @@ export function ChangeEmailDialog({ open, onOpenChange, currentEmail }: ChangeEm
<Button type="submit" disabled={loading}>
{loading ? (
<span className="flex items-center gap-2">
<Loader2 className="animate-spin" />
<Loader color="white" />
Procesando...
</span>
) : (
Expand Down Expand Up @@ -185,7 +184,7 @@ export function ChangeEmailDialog({ open, onOpenChange, currentEmail }: ChangeEm
>
{loading ? (
<span className="flex items-center gap-2">
<Loader2 className="animate-spin" />
<Loader color="white" />
Procesando...
</span>
) : (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
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 { passwordSchema, PasswordFormValues } from '@/lib/schemas/password-change'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
Expand All @@ -14,26 +14,8 @@ import {
FormMessage,
} from '@/components/ui/form'
import usePasswordManagement from '@/app/(with-navbar)/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<typeof passwordSchema>
import { Eye, EyeOff } from 'lucide-react'
import { Loader } from '@/components/ui/loader'

export function ChangePasswordDialog({
open,
Expand Down Expand Up @@ -159,7 +141,7 @@ export function ChangePasswordDialog({
>
{loading ? (
<span className="flex items-center gap-2">
<Loader2 className="animate-spin" />
<Loader color="white" />
Actualizando...
</span>
) : (
Expand Down
Loading