refactor: standardise S3 URL generation (#362)
* refactor: replace presigned URL uploads with backend upload endpoints * feat: update avatar upload to use new endpoint * refactor: use createS3Client in auth hooks * feat: generate presigned URLs for avatars * fix: show avatar image in user menu * fix: hide tooltip if content is empty * fix: support external avatar URLs in generateAvatarUrl * fix: remove content type restriction on attachments
This commit is contained in:
127
packages/shared/src/utils/s3.ts
Normal file
127
packages/shared/src/utils/s3.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
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 the URL as-is if it's already a full URL (external provider)
|
||||
* Returns presigned URL if it's an S3 key
|
||||
* 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;
|
||||
}
|
||||
|
||||
if (imageKey.startsWith("http://") || imageKey.startsWith("https://")) {
|
||||
return imageKey;
|
||||
}
|
||||
|
||||
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