feat: generate presigned URLs for avatars

This commit is contained in:
Henry
2026-02-04 21:50:37 +00:00
parent ceac4c0d81
commit c8517ed4c2
15 changed files with 211 additions and 73 deletions

View File

@@ -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",

View File

@@ -3,3 +3,4 @@ export * from "./generateSlug";
export * from "./subscriptions";
export * from "./email";
export * from "./dueDateFilters";
export * from "./s3";

View 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;
}
}