Inicio NextJS + S3
Post
Cancel

NextJS + S3

IAM

  • Crear un grupo de usuarios en IAM 

S3Crear grupo

  • Adjuntar la politica AmazonS3FullAccess

S3Politica S3

  • Crear un usuario en IAM

S3Usuario IAM

  • Agregar el usuario al grupo creado anteriormente

S3Agregar usuario al grupo

  • Una vez creado el usuario seleccionarlo y ver su perfil. Seleccionar Credenciales de seguridad

S3Credenciales de seguridad

  • Seleccionar la opcion de Crear llaves de acceso

S3Crear llaves de acceso

  • En caso de uso seleccionar CLI

S3Seleccionar CLI

  • Una vez generadas copiar ambas llaves de acceso o descargar archivo csv

S3Copiar llaves de acceso

Crear S3 Bucket

  • Crear un bucket con un nombre unico, copiar el nombre del bucket y la region donde se creo

S3Crear bucket

  • Asegurarse que la opciones de ACLs desabilitado y Bloquear acceso publico esten seleccionadas

S3Configuracion de bucket

  • Una vez creado seleccionar el bucket y dirigirse a la pestaña de permisos
  • En la seccion de CORS pegar lo siguiente
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "PUT",
            "POST",
            "GET"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": [
            "ETag"
        ]
    }
]
  • Crear una politica para el bucket. Puede utilizarse https://awspolicygen.s3.amazonaws.com/policygen.html para generarla. Se debera de colocar en Principal el ARN del Usuario,  en Acciones seleccionar GetObject y PutObject y en el ARN el arn del bucket añadiendo /* al final para conder acceso completo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
  "Id": "Policy1716700011680",
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Stm699991779",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::nombre-bucket/*",
      "Principal": {
        "AWS": [
          "arn:aws:iam::103650:user/nombre-de-usuario"
        ]
      }
    }
  ]
}

NEXT

  • Crear proyecto de Next con npx create-next-app@latest s3-app-dev 
  • Instalar las siguientes paquetes: 

npm i @aws-sdk/client-s3

   npm i @aws-sdk/s3-request-presigner

npm i axios

npx shadcn-ui@latest init

npx shadcn-ui@latest add input

npx shadcn-ui@latest add button

1
2
3
4
AWS_ACCESS_KEY =
AWS_SECRET_KEY =
AWS_BUCKET_NAME =
AWS_BUCKET_REGION =
  • Crear app/api/s3/route.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
const { S3Client } = require("@aws-sdk/client-s3");
import { NextRequest, NextResponse } from "next/server";
import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

// crea un cliente de S3 con las credenciales especificadas en las variables de entorno de AWS y el nombre de la region del bucket de S3
const client = new S3Client({
    region: process.env.AWS_BUCKET_REGION,
    credentials: {
        accessKeyId: process.env.AWS_ACCESS_KEY,
        secretAccessKey: process.env.AWS_SECRET_KEY,
    },
});

// nombre del bucket de S3
const bucketName = process.env.AWS_BUCKET_NAME;

// POST /api/s3
// API para subir archivos a S3
// Se espera que el archivo sea enviado como un form-data con el nombre "image". Retorna un JSON con la url del archivo subido o un mensaje de error
export async function POST(req: NextRequest) {
    const formData = await req.formData();
    const image = formData.get("image");
    const random = new Date().getTime();

    // verifica que el archivo sea valido y que sea un objeto con el nombre del archivo y que sea un archivo valido
    if (image && typeof image === "object" && image.name) {
        const Body = (await image.arrayBuffer()) as Buffer;
        const params = {
            Bucket: bucketName,
            //Key: image.name,
            Key: `${random}-${image.name}`,
            Body,
            ContentType: image.type,
        };

        // sube el archivo a S3 con los parametros especificados en la variable "params" y espera a que se complete la subida del archivo
        const command = new PutObjectCommand(params);
        await client.send(command);

        const getObjectParams = {
            Bucket: bucketName,
            Key: `${random}-${image.name}`,
            ACL: "private"
        };

        // obtiene la url firmada del archivo subido para poder ser accedido publicamente por un tiempo limitado
        const getCommand = new GetObjectCommand(getObjectParams);
        const url = await getSignedUrl(client, getCommand, { expiresIn: 3600 });

        // retorna un mensaje de exito en formato JSON con la url del archivo subido y la url firmada del archivo subido para ser accedido publicamente
        return NextResponse.json({
            success: true,
            message: "La imagen se subio correctamente!",
            data: {
                url
            },
        });

        // si no se pudo subir la imagen retorna un mensaje de error en formato JSON
        return NextResponse.json({
            success: false,
            message: "No se pudo subir la imagen",
            data: null,
        });
    }
}
  • Modificar app/page.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
"use client"

import { ChangeEvent, FormEvent, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import axios from "axios";
import Image from "next/image";

// Home page
// Esta pagina permite subir una imagen a un servidor de S3 y muestra la imagen subida
export default function Home() {
  // estado para guardar la url de la imagen subida
  const [image, setImage] = useState<string>();
  // estado para guardar la imagen seleccionada
  const [selectedImage, setSelectedImage] = useState<File>();

  // funcion para subir una imagen a un servidor de S3
  const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
    // previene el comportamiento por defecto del formulario
    event.preventDefault();
    // verifica que la imagen seleccionada exista y que sea un objeto valido de tipo File
    try {
      if (selectedImage) {
        const formData = new FormData();
        formData.append("image", selectedImage)
        const headers = {
          "Content-Type": "multipart/form-data"
        }

        // envia la imagen seleccionada al servidor de S3 y espera a que se suba la imagen
        const { data } = await axios.post("/api/s3", formData, { headers })
        if (data.success)
          setImage(data.data.url)
      }
    } catch (error) {
      console.error(error)
    }
  }

  // funcion para manejar el cambio de la imagen seleccionada
  const handleFileChange = (ev: ChangeEvent<HTMLInputElement>) => {
    const file = ev.target.files && ev.target.files[0];
    if (file)
      setSelectedImage(file)
  }
  // retorna el componente principal de la pagina Home con un formulario para subir una imagen y mostrar la imagen subida
  return (
    <main className="flex w-screen h-screen flex-col gap-3 items-center justify-center">
      <h1 className="text-3xl font-bold">Subir imagen a S3</h1>
      {
        image && <Image alt={image} src={image} height={400} width={400}
          className="rounded-md" />
      }
      <form onSubmit={onSubmit} className="flex flex-col gap-3 w-50">
        <label htmlFor="image" className="font-medium">Selecciona tu imagen</label >
        <Input id="image" type="file" onChange={handleFileChange} />
        <Button type="submit" className="w-100">Submit</Button>
      </form>
    </main>
  );
}
  • Modificar app/layout.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" className="dark">
      <body className={inter.className}>{children}</body>
    </html>
  );
}
  • Modificar next.config.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/** @type {import('next').NextConfig} */
const nextConfig = {
    // permite que o Next.js optimize as imagens automaticamente
    images: {
        // habilita o uso de formatos de imagem modernos (AVIF e WebP)
        formats: ["image/avif", "image/webp"],
        // habilita o uso de imagens responsivas 
        remotePatterns: [
            {
                // Permite a NextJS optimizar imagens de un bucket S3 
                protocol: "https",
                hostname: "nombre-bucket.s3.us-west-1.amazonaws.com",
                port: "",
                pathname: "/**",
            },
        ],
    },
};

export default nextConfig;
This post is licensed under CC BY 4.0 by the author.