feat: generate presigned URLs for avatars
This commit is contained in:
@@ -36,6 +36,8 @@
|
||||
},
|
||||
"prettier": "@kan/prettier-config",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.802.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.812.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"jose": "^6.1.2",
|
||||
"nanoid": "^5.0.9",
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from "./generateSlug";
|
||||
export * from "./subscriptions";
|
||||
export * from "./email";
|
||||
export * from "./dueDateFilters";
|
||||
export * from "./s3";
|
||||
|
||||
121
packages/shared/src/utils/s3.ts
Normal file
121
packages/shared/src/utils/s3.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
DeleteObjectCommand,
|
||||
GetObjectCommand,
|
||||
PutObjectCommand,
|
||||
S3Client,
|
||||
} from "@aws-sdk/client-s3";
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
import { env } from "next-runtime-env";
|
||||
|
||||
export function createS3Client() {
|
||||
const credentials =
|
||||
process.env.S3_ACCESS_KEY_ID && process.env.S3_SECRET_ACCESS_KEY
|
||||
? {
|
||||
accessKeyId: process.env.S3_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return new S3Client({
|
||||
region: process.env.S3_REGION ?? "",
|
||||
endpoint: process.env.S3_ENDPOINT ?? "",
|
||||
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === "true",
|
||||
credentials,
|
||||
});
|
||||
}
|
||||
|
||||
export async function generateUploadUrl(
|
||||
bucket: string,
|
||||
key: string,
|
||||
contentType: string,
|
||||
expiresIn = 3600,
|
||||
) {
|
||||
const client = createS3Client();
|
||||
return getSignedUrl(
|
||||
client,
|
||||
new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
ContentType: contentType,
|
||||
// Don't set ACL for private files
|
||||
}),
|
||||
{ expiresIn },
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateDownloadUrl(
|
||||
bucket: string,
|
||||
key: string,
|
||||
expiresIn = 3600,
|
||||
) {
|
||||
const client = createS3Client();
|
||||
return getSignedUrl(
|
||||
client,
|
||||
new GetObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
}),
|
||||
{ expiresIn },
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteObject(bucket: string, key: string) {
|
||||
const client = createS3Client();
|
||||
await client.send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate presigned URL for an avatar image
|
||||
* Returns null if image key is missing, bucket is not configured, or URL generation fails
|
||||
*/
|
||||
export async function generateAvatarUrl(
|
||||
imageKey: string | null | undefined,
|
||||
expiresIn = 86400, // 24 hours
|
||||
): Promise<string | null> {
|
||||
if (!imageKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bucket = env("NEXT_PUBLIC_AVATAR_BUCKET_NAME");
|
||||
if (!bucket) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return await generateDownloadUrl(bucket, imageKey, expiresIn);
|
||||
} catch {
|
||||
// If URL generation fails, return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate presigned URL for an attachment
|
||||
* Returns null if attachment key is missing, bucket is not configured, or URL generation fails
|
||||
*/
|
||||
export async function generateAttachmentUrl(
|
||||
attachmentKey: string | null | undefined,
|
||||
expiresIn = 86400, // 24 hours
|
||||
): Promise<string | null> {
|
||||
if (!attachmentKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bucket = env("NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME");
|
||||
if (!bucket) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return await generateDownloadUrl(bucket, attachmentKey, expiresIn);
|
||||
} catch {
|
||||
// If URL generation fails, return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user