diff --git a/.env.example b/.env.example index a7d7faf1..231d3789 100644 --- a/.env.example +++ b/.env.example @@ -29,6 +29,7 @@ S3_SECRET_ACCESS_KEY= S3_FORCE_PATH_STYLE= NEXT_PUBLIC_STORAGE_URL= NEXT_PUBLIC_AVATAR_BUCKET_NAME= +NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME= NEXT_PUBLIC_STORAGE_DOMAIN= # Auth config (optional) diff --git a/README.md b/README.md index 3e0f4178..f37780c8 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,7 @@ pnpm dev | `NEXT_PUBLIC_STORAGE_URL` | Storage service URL | For file uploads | `https://storage.kanbn.com` | | `NEXT_PUBLIC_STORAGE_DOMAIN` | Storage domain name | For file uploads | `kanbn.com` | | `NEXT_PUBLIC_AVATAR_BUCKET_NAME` | S3 bucket name for avatars | For file uploads | `avatars` | +| `NEXT_PUBLIC_ATTATCHMENTS_BUCKET_NAME` | S3 bucket name for attatchments | For file uploads | `attatchments` | | `NEXT_PUBLIC_ALLOW_CREDENTIALS` | Allow email & password login | For authentication | `true` | | `NEXT_PUBLIC_DISABLE_SIGN_UP` | Disable sign up | For authentication | `false` | | `NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY` | Hide “Powered by kan.bn” on public boards (self-host) | For white labelling | `true` | diff --git a/cloud/docker-compose.yml b/cloud/docker-compose.yml index e48cf635..d122ffa9 100644 --- a/cloud/docker-compose.yml +++ b/cloud/docker-compose.yml @@ -46,6 +46,7 @@ services: - S3_FORCE_PATH_STYLE=${S3_FORCE_PATH_STYLE} - NEXT_PUBLIC_STORAGE_URL=${NEXT_PUBLIC_STORAGE_URL} - NEXT_PUBLIC_AVATAR_BUCKET_NAME=${NEXT_PUBLIC_AVATAR_BUCKET_NAME} + - NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME=${NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME} - NEXT_PUBLIC_STORAGE_DOMAIN=${NEXT_PUBLIC_STORAGE_DOMAIN} # Auth config diff --git a/docker-compose.yml b/docker-compose.yml index c6616215..fca7330d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,6 +36,7 @@ services: - S3_FORCE_PATH_STYLE=${S3_FORCE_PATH_STYLE} - NEXT_PUBLIC_STORAGE_URL=${NEXT_PUBLIC_STORAGE_URL} - NEXT_PUBLIC_AVATAR_BUCKET_NAME=${NEXT_PUBLIC_AVATAR_BUCKET_NAME} + - NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME=${NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME} - NEXT_PUBLIC_STORAGE_DOMAIN=${NEXT_PUBLIC_STORAGE_DOMAIN} # White label diff --git a/packages/api/src/routers/attachment.ts b/packages/api/src/routers/attachment.ts index 6231119d..df99938f 100644 --- a/packages/api/src/routers/attachment.ts +++ b/packages/api/src/routers/attachment.ts @@ -9,7 +9,7 @@ import { generateUID } from "@kan/shared/utils"; import { createTRPCRouter, protectedProcedure } from "../trpc"; import { assertUserInWorkspace } from "../utils/auth"; -import { generateDownloadUrl, generateUploadUrl } from "../utils/s3"; +import { generateUploadUrl } from "../utils/s3"; export const attachmentRouter = createTRPCRouter({ generateUploadUrl: protectedProcedure @@ -78,7 +78,6 @@ export const attachmentRouter = createTRPCRouter({ .replace(/[^a-zA-Z0-9._-]/g, "_") .substring(0, 200); - // Generate S3 key: {workspacePublicId}/{cardPublicId}/{generateUID()}-{sanitizedFilename} const s3Key = `${workspace.publicId}/${input.cardPublicId}/${generateUID()}-${sanitizedFilename}`; const url = await generateUploadUrl( @@ -153,58 +152,6 @@ export const attachmentRouter = createTRPCRouter({ return attachment; }), - getUrl: protectedProcedure - .meta({ - openapi: { - summary: "Get presigned URL for attachment download", - method: "GET", - path: "/attachments/{attachmentPublicId}/url", - description: "Generates a presigned URL for downloading an attachment", - tags: ["Attachments"], - protect: true, - }, - }) - .input(z.object({ attachmentPublicId: z.string().min(12) })) - .output(z.object({ url: z.string(), filename: z.string() })) - .query(async ({ ctx, input }) => { - const userId = ctx.user?.id; - - if (!userId) - throw new TRPCError({ - message: `User not authenticated`, - code: "UNAUTHORIZED", - }); - - const attachment = await cardAttachmentRepo.getByPublicId( - ctx.db, - input.attachmentPublicId, - ); - - if (!attachment || attachment.deletedAt) - throw new TRPCError({ - message: `Attachment with public ID ${input.attachmentPublicId} not found`, - code: "NOT_FOUND", - }); - - const workspaceId = attachment.card.list.board.workspaceId; - - await assertUserInWorkspace(ctx.db, userId, workspaceId); - - const bucket = process.env.NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME; - if (!bucket) - throw new TRPCError({ - message: `Attachments bucket not configured`, - code: "INTERNAL_SERVER_ERROR", - }); - - const url = await generateDownloadUrl( - bucket, - attachment.s3Key, - 86400, // 24 hours expiration - ); - - return { url, filename: attachment.originalFilename }; - }), delete: protectedProcedure .meta({ openapi: { diff --git a/turbo.json b/turbo.json index d438c2ec..d28bdcde 100644 --- a/turbo.json +++ b/turbo.json @@ -108,6 +108,7 @@ "NEXT_PUBLIC_STORAGE_DOMAIN", "NEXT_PUBLIC_STORAGE_URL", "NEXT_PUBLIC_AVATAR_BUCKET_NAME", + "NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME", "NEXT_PUBLIC_ALLOW_CREDENTIALS", "NEXT_PUBLIC_DISABLE_SIGN_UP", "NEXT_PUBLIC_USE_STANDALONE_OUTPUT",