From 7e7ba2c0dab560d01957c953fa9e173cc85a9c47 Mon Sep 17 00:00:00 2001 From: Henry <30578846+hjball@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:22:59 +0100 Subject: [PATCH 1/5] chore: add screenshot to README --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2ca1a116..ff817e5c 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,10 @@ See our [roadmap](https://kan.bn/kan/roadmap) for upcoming features. +## Screenshot 👁️ + +hero-dark + ## Made With 🛠️ - [Next.js](https://nextjs.org/?ref=kan.bn) @@ -41,7 +45,7 @@ See our [roadmap](https://kan.bn/kan/roadmap) for upcoming features. - [Drizzle ORM](https://orm.drizzle.team/?ref=kan.bn) - [React Email](https://react.email/?ref=kan.bn) -## Local Development 🛠️ +## Local Development 🧑‍💻 1. Clone the repository (or fork) From a29a2f6e16dccdec0c26d1a2570467c5db784630 Mon Sep 17 00:00:00 2001 From: Tiago Manuel Date: Mon, 2 Jun 2025 17:20:10 +0200 Subject: [PATCH 2/5] Update .env.example (#4) remove double S3_ENDPOINT entry --- .env.example | 1 - 1 file changed, 1 deletion(-) diff --git a/.env.example b/.env.example index df46b227..d8e1e8ab 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,6 @@ NEXT_PUBLIC_AVATAR_BUCKET_NAME= S3_REGION= S3_ENDPOINT= -S3_ENDPOINT= S3_ACCESS_KEY_ID= S3_SECRET_ACCESS_KEY= From f590937ddb54f2b3b9238431ab7ed8909e7537af Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 3 Jun 2025 13:07:13 +0100 Subject: [PATCH 3/5] chore: add env variable table to readme --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index ff817e5c..434ee875 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,30 @@ pnpm install pnpm dev ``` +## Environment Variables 🔐 + +| Variable | Description | Required | Example | +| -------------------------------- | ----------------------------- | ---------------- | --------------------------------------------- | +| `POSTGRES_URL` | PostgreSQL connection URL | Yes | `postgres://user:pass@localhost:5432/db` | +| `EMAIL_FROM` | Sender email address | Yes | `"Kan "` | +| `EMAIL_URL` | Email service API URL | Yes | `https://api.resend.com/emails` | +| `EMAIL_TOKEN` | Email service API token | Yes | `re_xxxx` | +| `NEXT_PUBLIC_BASE_URL` | Base URL of your installation | Yes | `http://localhost:3000` | +| `BETTER_AUTH_SECRET` | Auth encryption secret | Yes | Random 32+ char string | +| `BETTER_AUTH_URL` | Auth callback URL | Yes | Same as `NEXT_PUBLIC_BASE_URL` | +| `BETTER_AUTH_TRUSTED_ORIGINS` | Allowed callback origins | For Google login | `http://localhost:3000,http://localhost:3001` | +| `GOOGLE_CLIENT_ID` | Google OAuth client ID | For Google login | `xxx.apps.googleusercontent.com` | +| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret | For Google login | `xxx` | +| `S3_REGION` | S3 storage region | For file uploads | `WEUR` | +| `S3_ENDPOINT` | S3 endpoint URL | For file uploads | `https://xxx.r2.cloudflarestorage.com` | +| `S3_ACCESS_KEY_ID` | S3 access key | For file uploads | `xxx` | +| `S3_SECRET_ACCESS_KEY` | S3 secret key | For file uploads | `xxx` | +| `NEXT_PUBLIC_STORAGE_URL` | Storage service URL | For file uploads | `https://storage.kanbn.com` | +| `NEXT_PUBLIC_STORAGE_DOMAIN` | Storage domain name | For file uploads | `storage.kanbn.com` | +| `NEXT_PUBLIC_AVATAR_BUCKET_NAME` | S3 bucket name for avatars | For file uploads | `avatars` | + +See `.env.example` for a complete list of supported environment variables. + ## Contributing 🤝 We welcome contributions! Please read our [contribution guidelines](CONTRIBUTING.md) before submitting a pull request. From 1027b45cdbca2d7c90b036fc162a61a35be90304 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 3 Jun 2025 13:45:26 +0100 Subject: [PATCH 4/5] feat: update image upload API with user authentication and validation --- apps/web/src/pages/api/upload/image.ts | 37 ++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/apps/web/src/pages/api/upload/image.ts b/apps/web/src/pages/api/upload/image.ts index 703d19a6..c4aae974 100644 --- a/apps/web/src/pages/api/upload/image.ts +++ b/apps/web/src/pages/api/upload/image.ts @@ -2,8 +2,12 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import { createNextApiContext } from "@kan/api/trpc"; + import { env } from "~/env"; +const allowedContentTypes = ["image/jpeg", "image/png"]; + export default async function handler( req: NextApiRequest, res: NextApiResponse, @@ -13,14 +17,37 @@ export default async function handler( } try { - const { filename, contentType } = req.body; + const { user } = await createNextApiContext(req); + + if (!user) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const { filename, contentType } = req.body as { + filename: string; + contentType: string; + }; + + // Specific to avatar uploads for now + const filenameRegex = /^[a-f0-9\-]+\/[a-zA-Z0-9_\-]+(\.jpg|\.jpeg|\.png)$/; + + if (!filenameRegex.test(filename)) { + return res.status(400).json({ error: "Invalid filename" }); + } + + if ( + typeof contentType !== "string" || + !allowedContentTypes.includes(contentType) + ) { + return res.status(400).json({ error: "Invalid content type" }); + } const client = new S3Client({ - region: env.S3_REGION, - endpoint: env.S3_ENDPOINT, + region: env.S3_REGION ?? "", + endpoint: env.S3_ENDPOINT ?? "", credentials: { - accessKeyId: env.S3_ACCESS_KEY_ID, - secretAccessKey: env.S3_SECRET_ACCESS_KEY, + accessKeyId: env.S3_ACCESS_KEY_ID ?? "", + secretAccessKey: env.S3_SECRET_ACCESS_KEY ?? "", }, }); From 37d2ebccf71258a279e4f4c872a6ccf30c688367 Mon Sep 17 00:00:00 2001 From: Krisztiaan Date: Tue, 3 Jun 2025 17:07:12 +0200 Subject: [PATCH 5/5] fix: modal forms not submitting on Enter key (#9) * fix: add type="button" to modal close buttons to fix Enter key form submission Previously, pressing Enter in modal form inputs would dismiss the modal instead of submitting the form because close buttons defaulted to type="submit". This adds explicit type="button" to all modal close buttons to ensure Enter key properly submits forms. Fixes: - New board creation via Enter key - New workspace creation via Enter key - New card creation via Enter key - New list creation via Enter key - Member invitation via Enter key - Label creation/editing via Enter key - Board URL updates via Enter key - Import boards forms via Enter key * fix: submit forms on enter key submission --------- Co-authored-by: Henry --- apps/web/src/components/FeedbackButton.tsx | 10 ++++++--- apps/web/src/components/Input.tsx | 1 + apps/web/src/components/LabelForm.tsx | 21 +++++++++++++++++-- apps/web/src/components/NewWorkspaceForm.tsx | 7 +++++++ .../views/board/components/NewCardForm.tsx | 19 ++++++++++++++++- .../views/board/components/NewListForm.tsx | 13 +++++++++++- .../board/components/UpdateBoardSlugForm.tsx | 7 +++++++ .../boards/components/ImportBoardsForm.tsx | 1 + .../views/boards/components/NewBoardForm.tsx | 18 ++++++++++++++-- .../views/card/components/NewCommentForm.tsx | 6 ++++++ .../members/components/InviteMemberForm.tsx | 18 ++++++++++++++-- 11 files changed, 110 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/FeedbackButton.tsx b/apps/web/src/components/FeedbackButton.tsx index 4495cd57..887f7de1 100644 --- a/apps/web/src/components/FeedbackButton.tsx +++ b/apps/web/src/components/FeedbackButton.tsx @@ -98,12 +98,16 @@ const FeedbackButton: React.FC = () => { onChange={(e) => { setValue("feedback", e.target.value); }} - onKeyDown={(e) => { - e.stopPropagation(); - }} value={watch("feedback")} contentEditable className="max-h-[300px] min-h-[100px]" + onKeyDown={async (e) => { + e.stopPropagation(); + if (e.key === "Enter" && e.shiftKey) { + e.preventDefault(); + await handleSubmit(onSubmit)(); + } + }} />
diff --git a/apps/web/src/components/Input.tsx b/apps/web/src/components/Input.tsx index d2712b2e..8a24d212 100644 --- a/apps/web/src/components/Input.tsx +++ b/apps/web/src/components/Input.tsx @@ -69,6 +69,7 @@ const Input = forwardRef( prefix && "rounded-l-none", className && className, )} + onKeyDown={onKeyDown} {...props} /> {type === "password" && ( diff --git a/apps/web/src/components/LabelForm.tsx b/apps/web/src/components/LabelForm.tsx index ef1a5496..5b2d8b41 100644 --- a/apps/web/src/components/LabelForm.tsx +++ b/apps/web/src/components/LabelForm.tsx @@ -1,5 +1,5 @@ import { Listbox, Transition } from "@headlessui/react"; -import { Fragment } from "react"; +import { Fragment, useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; import { HiChevronUpDown, HiXMark } from "react-icons/hi2"; @@ -103,6 +103,12 @@ export function LabelForm({ } }; + useEffect(() => { + const nameElement: HTMLElement | null = + document.querySelector("#label-name"); + if (nameElement) nameElement.focus(); + }, []); + return (
@@ -111,6 +117,7 @@ export function LabelForm({ {isEdit ? "Edit label" : "New label"}
- + { + if (e.key === "Enter") { + e.preventDefault(); + await handleSubmit(onSubmit)(); + } + }} + />
diff --git a/apps/web/src/views/board/components/NewCardForm.tsx b/apps/web/src/views/board/components/NewCardForm.tsx index ba76f42e..6d028e3a 100644 --- a/apps/web/src/views/board/components/NewCardForm.tsx +++ b/apps/web/src/views/board/components/NewCardForm.tsx @@ -226,6 +226,7 @@ export function NewCardForm({ New card
- + { + if (e.key === "Enter") { + e.preventDefault(); + await handleSubmit(onSubmit)(); + } + }} + />
setValue("description", e.target.value)} value={watch("description")} contentEditable + onKeyDown={async (e) => { + if (e.key === "Enter" && e.shiftKey) { + e.preventDefault(); + await handleSubmit(onSubmit)(); + } + }} />
diff --git a/apps/web/src/views/board/components/NewListForm.tsx b/apps/web/src/views/board/components/NewListForm.tsx index ec642421..b6c64693 100644 --- a/apps/web/src/views/board/components/NewListForm.tsx +++ b/apps/web/src/views/board/components/NewListForm.tsx @@ -111,6 +111,7 @@ export function NewListForm({ New list
- + { + if (e.key === "Enter") { + e.preventDefault(); + await handleSubmit(onSubmit)(); + } + }} + />
diff --git a/apps/web/src/views/boards/components/ImportBoardsForm.tsx b/apps/web/src/views/boards/components/ImportBoardsForm.tsx index a68c595a..d8aab5e1 100644 --- a/apps/web/src/views/boards/components/ImportBoardsForm.tsx +++ b/apps/web/src/views/boards/components/ImportBoardsForm.tsx @@ -251,6 +251,7 @@ export function ImportBoardsForm() {
- { + if (e.key === "Enter") { + e.preventDefault(); + await handleSubmit(onSubmit)(); + } + }} /> diff --git a/apps/web/src/views/card/components/NewCommentForm.tsx b/apps/web/src/views/card/components/NewCommentForm.tsx index b105a11b..128a1ae8 100644 --- a/apps/web/src/views/card/components/NewCommentForm.tsx +++ b/apps/web/src/views/card/components/NewCommentForm.tsx @@ -58,6 +58,12 @@ const NewCommentForm = ({ cardPublicId }: { cardPublicId: string }) => { disabled={false} onChange={(e) => setValue("comment", e.target.value)} className="block w-full border-0 bg-transparent py-1.5 text-light-900 focus-visible:outline-none dark:text-dark-1000 sm:text-sm sm:leading-6" + onKeyDown={async (e) => { + if (e.key === "Enter" && e.shiftKey) { + e.preventDefault(); + await handleSubmit(onSubmit)(); + } + }} />
- { + if (e.key === "Enter") { + e.preventDefault(); + await handleSubmit(onSubmit)(); + } + }} />