diff --git a/app/api/generate-comic/route.ts b/app/api/generate-comic/route.ts index 3bd8ee3..f775c5a 100644 --- a/app/api/generate-comic/route.ts +++ b/app/api/generate-comic/route.ts @@ -10,6 +10,7 @@ import { } from "@/lib/db-actions"; import { freeTierRateLimit } from "@/lib/rate-limit"; import { COMIC_STYLES } from "@/lib/constants"; +import { uploadImageToS3 } from "@/lib/s3-upload"; const NEW_MODEL = false; @@ -243,9 +244,13 @@ COMPOSITION: const imageUrl = response.data[0].url; - // Update page in database + // Upload image to S3 for permanent storage + const s3Key = `${storyId || story!.id}/page-${page.pageNumber}-${Date.now()}.jpg`; + const s3ImageUrl = await uploadImageToS3(imageUrl, s3Key); + + // Update page in database with S3 URL try { - await updatePage(page.id, imageUrl); + await updatePage(page.id, s3ImageUrl); } catch (dbError) { console.error("Error updating page in database:", dbError); return NextResponse.json( @@ -255,9 +260,9 @@ COMPOSITION: } const responseData = storyId - ? { imageUrl, pageId: page.id, pageNumber: page.pageNumber } + ? { imageUrl: s3ImageUrl, pageId: page.id, pageNumber: page.pageNumber } : { - imageUrl, + imageUrl: s3ImageUrl, storyId: story!.id, storySlug: story!.slug, pageId: page.id, diff --git a/lib/s3-upload.ts b/lib/s3-upload.ts new file mode 100644 index 0000000..066fd50 --- /dev/null +++ b/lib/s3-upload.ts @@ -0,0 +1,46 @@ +import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; + +if (!process.env.S3_UPLOAD_KEY || !process.env.S3_UPLOAD_SECRET || !process.env.S3_UPLOAD_BUCKET || !process.env.S3_UPLOAD_REGION) { + throw new Error("Missing required S3 environment variables"); +} + +const s3Client = new S3Client({ + region: process.env.S3_UPLOAD_REGION || "us-east-1", + credentials: { + accessKeyId: process.env.S3_UPLOAD_KEY!, + secretAccessKey: process.env.S3_UPLOAD_SECRET!, + }, +}); + +export async function uploadImageToS3( + imageUrl: string, + key: string +): Promise { + try { + const response = await fetch(imageUrl); + if (!response.ok) { + throw new Error(`Failed to fetch image: ${response.statusText}`); + } + + const imageBuffer = Buffer.from(await response.arrayBuffer()); + + const command = new PutObjectCommand({ + Bucket: process.env.S3_UPLOAD_BUCKET!, + Key: `comics/${key}`, + Body: imageBuffer, + ContentType: "image/jpeg", + Metadata: { + "app-name": "make-comics", + "type": "comic-page", + }, + }); + + await s3Client.send(command); + + const publicUrl = `https://${process.env.S3_UPLOAD_BUCKET}.s3.${process.env.S3_UPLOAD_REGION}.amazonaws.com/comics/${key}`; + return publicUrl; + } catch (error) { + console.error("Error uploading to S3:", error); + throw new Error("Failed to upload image to S3"); + } +} diff --git a/package.json b/package.json index 378c6d4..f4dc843 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "start": "next start" }, "dependencies": { + "@aws-sdk/client-s3": "^3.958.0", "@clerk/nextjs": "^6.36.5", "@hookform/resolvers": "^3.10.0", "@neondatabase/serverless": "^1.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd88410..803c9c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@aws-sdk/client-s3': + specifier: ^3.958.0 + version: 3.958.0 '@clerk/nextjs': specifier: ^6.36.5 version: 6.36.5(next@16.1.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -240,14 +243,18 @@ packages: '@aws-crypto/util@5.2.0': resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - '@aws-sdk/client-s3@3.957.0': - resolution: {integrity: sha512-UvKQ9CpqzJxMjSjFpBaeWFAnR2sQChQH2jJEhsCFkAvrz1KDrU7sjOqUAPUH+ytsP5xghx1AZhfK2rVE/+JS0Q==} + '@aws-sdk/client-s3@3.958.0': + resolution: {integrity: sha512-ol8Sw37AToBWb6PjRuT/Wu40SrrZSA0N4F7U3yTkjUNX0lirfO1VFLZ0hZtZplVJv8GNPITbiczxQ8VjxESXxg==} engines: {node: '>=18.0.0'} '@aws-sdk/client-sso@3.957.0': resolution: {integrity: sha512-iRdRjd+IpOogqRPt8iNRcg30J53z4rRfMviGwpKgsEa/fx3inCUPOuca3Ap7ZDES0atnEg3KGSJ3V/NQiEJ4BA==} engines: {node: '>=18.0.0'} + '@aws-sdk/client-sso@3.958.0': + resolution: {integrity: sha512-6qNCIeaMzKzfqasy2nNRuYnMuaMebCcCPP4J2CVGkA8QYMbIVKPlkn9bpB20Vxe6H/r3jtCCLQaOJjVTx/6dXg==} + engines: {node: '>=18.0.0'} + '@aws-sdk/client-sts@3.957.0': resolution: {integrity: sha512-rqH0blCu4LrbW3wiFxPBAttqz81NrJk1fAxem869FyRwDXtNVDFgQeVNLyUKpf340O2uRiES/PkCMb240T86PQ==} engines: {node: '>=18.0.0'} @@ -272,14 +279,26 @@ packages: resolution: {integrity: sha512-YuoZmIeE91YIeUfihh8SiSu546KtTvU+4rG5SaL30U9+nGq6P11GRRgqF0ANUyRseLC9ONHt+utar4gbO3++og==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-ini@3.958.0': + resolution: {integrity: sha512-u7twvZa1/6GWmPBZs6DbjlegCoNzNjBsMS/6fvh5quByYrcJr/uLd8YEr7S3UIq4kR/gSnHqcae7y2nL2bqZdg==} + engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-login@3.957.0': resolution: {integrity: sha512-XcD5NEQDWYk8B4gs89bkwf2d+DNF8oS2NR5RoHJEbX4l8KErVATUjpEYVn6/rAFEktungxlYTnQ5wh0cIQvP5w==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-login@3.958.0': + resolution: {integrity: sha512-sDwtDnBSszUIbzbOORGh5gmXGl9aK25+BHb4gb1aVlqB+nNL2+IUEJA62+CE55lXSH8qXF90paivjK8tOHTwPA==} + engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-node@3.957.0': resolution: {integrity: sha512-b9FT/7BQcJ001w+3JbTiJXfxHrWvPb7zDvvC1i1FKcNOvyCt3BGu04n4nO/b71a3iBnbfBXI89hCIZQsuLcEgw==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-node@3.958.0': + resolution: {integrity: sha512-vdoZbNG2dt66I7EpN3fKCzi6fp9xjIiwEA/vVVgqO4wXCGw8rKPIdDUus4e13VvTr330uQs2W0UNg/7AgtquEQ==} + engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-process@3.957.0': resolution: {integrity: sha512-/KIz9kadwbeLy6SKvT79W81Y+hb/8LMDyeloA2zhouE28hmne+hLn0wNCQXAAupFFlYOAtZR2NTBs7HBAReJlg==} engines: {node: '>=18.0.0'} @@ -288,10 +307,18 @@ packages: resolution: {integrity: sha512-gTLPJFOkGtn3tVGglRhCar2oOobK1YctZRAT8nfJr17uaSRoAP46zIIHNYBZZUMqImb0qAHD9Ugm+Zd9sIqxyA==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-sso@3.958.0': + resolution: {integrity: sha512-CBYHJ5ufp8HC4q+o7IJejCUctJXWaksgpmoFpXerbjAso7/Fg7LLUu9inXVOxlHKLlvYekDXjIUBXDJS2WYdgg==} + engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-web-identity@3.957.0': resolution: {integrity: sha512-x17xMeD7c+rKEsWachGIMifACqkugskrETWz18QDWismFcrmUuOcZu5rUa8s9y1pnITLKUQ1xU/qDLPH52jLlA==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-web-identity@3.958.0': + resolution: {integrity: sha512-dgnvwjMq5Y66WozzUzxNkCFap+umHUtqMMKlr8z/vl9NYMLem/WUbWNpFFOVFWquXikc+ewtpBMR4KEDXfZ+KA==} + engines: {node: '>=18.0.0'} + '@aws-sdk/lib-storage@3.957.0': resolution: {integrity: sha512-qTkYG37Zi4wcoF45tx9e/jNxNax59ZqjfLvEzVL84QzICYU4Jz2VONmm56UUYstaNHSBwTtNOjQV3gAKjJ+aCw==} engines: {node: '>=18.0.0'} @@ -342,6 +369,10 @@ packages: resolution: {integrity: sha512-PZUFtaUTSZWO+mbgQGWSiwz3EqedsuKNb7Xoxjzh5rfJE352DD4/jScQEhVPxvdLw62IK9b5UDu5kZlxzBs9Ow==} engines: {node: '>=18.0.0'} + '@aws-sdk/nested-clients@3.958.0': + resolution: {integrity: sha512-/KuCcS8b5TpQXkYOrPLYytrgxBhv81+5pChkOlhegbeHttjM69pyUpQVJqyfDM/A7wPLnDrzCAnk4zaAOkY0Nw==} + engines: {node: '>=18.0.0'} + '@aws-sdk/region-config-resolver@3.957.0': resolution: {integrity: sha512-V8iY3blh8l2iaOqXWW88HbkY5jDoWjH56jonprG/cpyqqCnprvpMUZWPWYJoI8rHRf2bqzZeql1slxG6EnKI7A==} engines: {node: '>=18.0.0'} @@ -358,6 +389,10 @@ packages: resolution: {integrity: sha512-oSwo3BZ6gcvhjTg036V0UQmtENUeNwfCU35iDckX961CdI1alQ3TKRWLzKrwvXCbrOx+bZsuA1PHsTbNhI/+Fw==} engines: {node: '>=18.0.0'} + '@aws-sdk/token-providers@3.958.0': + resolution: {integrity: sha512-UCj7lQXODduD1myNJQkV+LYcGYJ9iiMggR8ow8Hva1g3A/Na5imNXzz6O67k7DAee0TYpy+gkNw+SizC6min8Q==} + engines: {node: '>=18.0.0'} + '@aws-sdk/types@3.957.0': resolution: {integrity: sha512-wzWC2Nrt859ABk6UCAVY/WYEbAd7FjkdrQL6m24+tfmWYDNRByTJ9uOgU/kw9zqLCAwb//CPvrJdhqjTznWXAg==} engines: {node: '>=18.0.0'} @@ -2838,13 +2873,13 @@ snapshots: '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 - '@aws-sdk/client-s3@3.957.0': + '@aws-sdk/client-s3@3.958.0': dependencies: '@aws-crypto/sha1-browser': 5.2.0 '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 '@aws-sdk/core': 3.957.0 - '@aws-sdk/credential-provider-node': 3.957.0 + '@aws-sdk/credential-provider-node': 3.958.0 '@aws-sdk/middleware-bucket-endpoint': 3.957.0 '@aws-sdk/middleware-expect-continue': 3.957.0 '@aws-sdk/middleware-flexible-checksums': 3.957.0 @@ -2941,6 +2976,49 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/client-sso@3.958.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.957.0 + '@aws-sdk/middleware-host-header': 3.957.0 + '@aws-sdk/middleware-logger': 3.957.0 + '@aws-sdk/middleware-recursion-detection': 3.957.0 + '@aws-sdk/middleware-user-agent': 3.957.0 + '@aws-sdk/region-config-resolver': 3.957.0 + '@aws-sdk/types': 3.957.0 + '@aws-sdk/util-endpoints': 3.957.0 + '@aws-sdk/util-user-agent-browser': 3.957.0 + '@aws-sdk/util-user-agent-node': 3.957.0 + '@smithy/config-resolver': 4.4.5 + '@smithy/core': 3.20.0 + '@smithy/fetch-http-handler': 5.3.8 + '@smithy/hash-node': 4.2.7 + '@smithy/invalid-dependency': 4.2.7 + '@smithy/middleware-content-length': 4.2.7 + '@smithy/middleware-endpoint': 4.4.1 + '@smithy/middleware-retry': 4.4.17 + '@smithy/middleware-serde': 4.2.8 + '@smithy/middleware-stack': 4.2.7 + '@smithy/node-config-provider': 4.3.7 + '@smithy/node-http-handler': 4.4.7 + '@smithy/protocol-http': 5.3.7 + '@smithy/smithy-client': 4.10.2 + '@smithy/types': 4.11.0 + '@smithy/url-parser': 4.2.7 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.16 + '@smithy/util-defaults-mode-node': 4.2.19 + '@smithy/util-endpoints': 3.2.7 + '@smithy/util-middleware': 4.2.7 + '@smithy/util-retry': 4.2.7 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/client-sts@3.957.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 @@ -3046,6 +3124,25 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-ini@3.958.0': + dependencies: + '@aws-sdk/core': 3.957.0 + '@aws-sdk/credential-provider-env': 3.957.0 + '@aws-sdk/credential-provider-http': 3.957.0 + '@aws-sdk/credential-provider-login': 3.958.0 + '@aws-sdk/credential-provider-process': 3.957.0 + '@aws-sdk/credential-provider-sso': 3.958.0 + '@aws-sdk/credential-provider-web-identity': 3.958.0 + '@aws-sdk/nested-clients': 3.958.0 + '@aws-sdk/types': 3.957.0 + '@smithy/credential-provider-imds': 4.2.7 + '@smithy/property-provider': 4.2.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/credential-provider-login@3.957.0': dependencies: '@aws-sdk/core': 3.957.0 @@ -3059,6 +3156,19 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-login@3.958.0': + dependencies: + '@aws-sdk/core': 3.957.0 + '@aws-sdk/nested-clients': 3.958.0 + '@aws-sdk/types': 3.957.0 + '@smithy/property-provider': 4.2.7 + '@smithy/protocol-http': 5.3.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/credential-provider-node@3.957.0': dependencies: '@aws-sdk/credential-provider-env': 3.957.0 @@ -3076,6 +3186,23 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-node@3.958.0': + dependencies: + '@aws-sdk/credential-provider-env': 3.957.0 + '@aws-sdk/credential-provider-http': 3.957.0 + '@aws-sdk/credential-provider-ini': 3.958.0 + '@aws-sdk/credential-provider-process': 3.957.0 + '@aws-sdk/credential-provider-sso': 3.958.0 + '@aws-sdk/credential-provider-web-identity': 3.958.0 + '@aws-sdk/types': 3.957.0 + '@smithy/credential-provider-imds': 4.2.7 + '@smithy/property-provider': 4.2.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/credential-provider-process@3.957.0': dependencies: '@aws-sdk/core': 3.957.0 @@ -3098,6 +3225,19 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-sso@3.958.0': + dependencies: + '@aws-sdk/client-sso': 3.958.0 + '@aws-sdk/core': 3.957.0 + '@aws-sdk/token-providers': 3.958.0 + '@aws-sdk/types': 3.957.0 + '@smithy/property-provider': 4.2.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/credential-provider-web-identity@3.957.0': dependencies: '@aws-sdk/core': 3.957.0 @@ -3110,9 +3250,21 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/lib-storage@3.957.0(@aws-sdk/client-s3@3.957.0)': + '@aws-sdk/credential-provider-web-identity@3.958.0': dependencies: - '@aws-sdk/client-s3': 3.957.0 + '@aws-sdk/core': 3.957.0 + '@aws-sdk/nested-clients': 3.958.0 + '@aws-sdk/types': 3.957.0 + '@smithy/property-provider': 4.2.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/lib-storage@3.957.0(@aws-sdk/client-s3@3.958.0)': + dependencies: + '@aws-sdk/client-s3': 3.958.0 '@smithy/abort-controller': 4.2.7 '@smithy/middleware-endpoint': 4.4.1 '@smithy/smithy-client': 4.10.2 @@ -3258,6 +3410,49 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/nested-clients@3.958.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.957.0 + '@aws-sdk/middleware-host-header': 3.957.0 + '@aws-sdk/middleware-logger': 3.957.0 + '@aws-sdk/middleware-recursion-detection': 3.957.0 + '@aws-sdk/middleware-user-agent': 3.957.0 + '@aws-sdk/region-config-resolver': 3.957.0 + '@aws-sdk/types': 3.957.0 + '@aws-sdk/util-endpoints': 3.957.0 + '@aws-sdk/util-user-agent-browser': 3.957.0 + '@aws-sdk/util-user-agent-node': 3.957.0 + '@smithy/config-resolver': 4.4.5 + '@smithy/core': 3.20.0 + '@smithy/fetch-http-handler': 5.3.8 + '@smithy/hash-node': 4.2.7 + '@smithy/invalid-dependency': 4.2.7 + '@smithy/middleware-content-length': 4.2.7 + '@smithy/middleware-endpoint': 4.4.1 + '@smithy/middleware-retry': 4.4.17 + '@smithy/middleware-serde': 4.2.8 + '@smithy/middleware-stack': 4.2.7 + '@smithy/node-config-provider': 4.3.7 + '@smithy/node-http-handler': 4.4.7 + '@smithy/protocol-http': 5.3.7 + '@smithy/smithy-client': 4.10.2 + '@smithy/types': 4.11.0 + '@smithy/url-parser': 4.2.7 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.16 + '@smithy/util-defaults-mode-node': 4.2.19 + '@smithy/util-endpoints': 3.2.7 + '@smithy/util-middleware': 4.2.7 + '@smithy/util-retry': 4.2.7 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/region-config-resolver@3.957.0': dependencies: '@aws-sdk/types': 3.957.0 @@ -3298,6 +3493,18 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/token-providers@3.958.0': + dependencies: + '@aws-sdk/core': 3.957.0 + '@aws-sdk/nested-clients': 3.958.0 + '@aws-sdk/types': 3.957.0 + '@smithy/property-provider': 4.2.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/types@3.957.0': dependencies: '@smithy/types': 4.11.0 @@ -5259,9 +5466,9 @@ snapshots: next-s3-upload@0.3.4(next@16.1.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0): dependencies: - '@aws-sdk/client-s3': 3.957.0 + '@aws-sdk/client-s3': 3.958.0 '@aws-sdk/client-sts': 3.957.0 - '@aws-sdk/lib-storage': 3.957.0(@aws-sdk/client-s3@3.957.0) + '@aws-sdk/lib-storage': 3.957.0(@aws-sdk/client-s3@3.958.0) '@aws-sdk/s3-request-presigner': 3.957.0 '@smithy/fetch-http-handler': 2.5.0 next: 16.1.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)