From 40aba5cdba3f307004488f3436e7928c60eddc2c Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 15 Aug 2025 13:32:33 +0100 Subject: [PATCH] feat: add basic white labelling --- README.md | 61 ++++++++++++----------- apps/web/src/env.ts | 11 +++- apps/web/src/views/public/board/index.tsx | 31 +++++++++--- docker-compose.yml | 3 ++ turbo.json | 1 + 5 files changed, 70 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index a061cad8..1fc5144c 100644 --- a/README.md +++ b/README.md @@ -138,36 +138,37 @@ pnpm dev ## Environment Variables 🔐 -| Variable | Description | Required | Example | -| -------------------------------- | -------------------------------------------------------- | ------------------ | --------------------------------------------- | -| `POSTGRES_URL` | PostgreSQL connection URL | To use external database | `postgres://user:pass@localhost:5432/db` | -| `EMAIL_FROM` | Sender email address | For Email | `"Kan "` | -| `SMTP_HOST` | SMTP server hostname | For Email | `smtp.resend.com` | -| `SMTP_PORT` | SMTP server port | For Email | `465` | -| `SMTP_USER` | SMTP username/email | No | `resend` | -| `SMTP_PASSWORD` | SMTP password/token | No | `re_xxxx` | -| `SMTP_SECURE` | Use secure SMTP connection (defaults to true if not set) | For Email | `true` | -| `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_TRUSTED_ORIGINS` | Allowed callback origins | No | `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` | -| `DISCORD_CLIENT_ID` | Discord OAuth client ID | For Discord login | `xxx` | -| `DISCORD_CLIENT_SECRET` | Discord OAuth client secret | For Discord login | `xxx` | -| `GITHUB_CLIENT_ID` | GitHub OAuth client ID | For GitHub login | `xxx` | -| `GITHUB_CLIENT_SECRET` | GitHub OAuth client secret | For GitHub login | `xxx` | -| `TRELLO_APP_API_KEY` | Trello app API key | For Trello import | `xxx` | -| `TRELLO_APP_API_SECRET` | Trello app API secret | For Trello import | `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` | -| `S3_FORCE_PATH_STYLE` | Use path-style URLs for S3 | For file uploads | `true` | -| `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_ALLOW_CREDENTIALS` | Allow email & password login | For authentication | `true` | -| `NEXT_PUBLIC_DISABLE_SIGN_UP` | Disable sign up | For authentication | `false` | +| Variable | Description | Required | Example | +| ----------------------------------------- | -------------------------------------------------------- | ------------------------ | --------------------------------------------- | +| `POSTGRES_URL` | PostgreSQL connection URL | To use external database | `postgres://user:pass@localhost:5432/db` | +| `EMAIL_FROM` | Sender email address | For Email | `"Kan "` | +| `SMTP_HOST` | SMTP server hostname | For Email | `smtp.resend.com` | +| `SMTP_PORT` | SMTP server port | For Email | `465` | +| `SMTP_USER` | SMTP username/email | No | `resend` | +| `SMTP_PASSWORD` | SMTP password/token | No | `re_xxxx` | +| `SMTP_SECURE` | Use secure SMTP connection (defaults to true if not set) | For Email | `true` | +| `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_TRUSTED_ORIGINS` | Allowed callback origins | No | `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` | +| `DISCORD_CLIENT_ID` | Discord OAuth client ID | For Discord login | `xxx` | +| `DISCORD_CLIENT_SECRET` | Discord OAuth client secret | For Discord login | `xxx` | +| `GITHUB_CLIENT_ID` | GitHub OAuth client ID | For GitHub login | `xxx` | +| `GITHUB_CLIENT_SECRET` | GitHub OAuth client secret | For GitHub login | `xxx` | +| `TRELLO_APP_API_KEY` | Trello app API key | For Trello import | `xxx` | +| `TRELLO_APP_API_SECRET` | Trello app API secret | For Trello import | `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` | +| `S3_FORCE_PATH_STYLE` | Use path-style URLs for S3 | For file uploads | `true` | +| `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_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` | See `.env.example` for a complete list of supported environment variables. diff --git a/apps/web/src/env.ts b/apps/web/src/env.ts index e9be5344..4aebcf59 100644 --- a/apps/web/src/env.ts +++ b/apps/web/src/env.ts @@ -24,7 +24,7 @@ export const env = createEnv({ s.split(",").every((l) => z.string().url().safeParse(l).success), ) .optional(), - POSTGRES_URL: z.string().url().optional().or(z.literal('')), + POSTGRES_URL: z.string().url().optional().or(z.literal("")), TRELLO_APP_API_KEY: z.string().optional(), TRELLO_APP_SECRET: z.string().optional(), STRIPE_SECRET_KEY: z.string().optional(), @@ -101,6 +101,13 @@ export const env = createEnv({ (s) => !s || s.toLowerCase() === "true" || s.toLowerCase() === "false", ) .optional(), + NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY: z + .string() + .transform((s) => (s === "" ? undefined : s)) + .refine( + (s) => !s || s.toLowerCase() === "true" || s.toLowerCase() === "false", + ) + .optional(), }, /** * Destructure all variables from `process.env` to make sure they aren't tree-shaken away. @@ -119,6 +126,8 @@ export const env = createEnv({ NEXT_PUBLIC_DISABLE_SIGN_UP: process.env.NEXT_PUBLIC_DISABLE_SIGN_UP, NEXT_PUBLIC_USE_STANDALONE_OUTPUT: process.env.NEXT_PUBLIC_USE_STANDALONE_OUTPUT, + NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY: + process.env.NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY, }, skipValidation: !!process.env.CI || process.env.npm_lifecycle_event === "lint", diff --git a/apps/web/src/views/public/board/index.tsx b/apps/web/src/views/public/board/index.tsx index 1944f763..f0b16b71 100644 --- a/apps/web/src/views/public/board/index.tsx +++ b/apps/web/src/views/public/board/index.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { useRouter } from "next/router"; import { t } from "@lingui/core/macro"; import { keepPreviousData } from "@tanstack/react-query"; +import { env } from "next-runtime-env"; import { useEffect, useState } from "react"; import { HiLink, HiOutlineLockClosed } from "react-icons/hi2"; @@ -19,6 +20,10 @@ import Card from "~/views/board/components/Card"; import Filters from "~/views/board/components/Filters"; import { CardModal } from "./CardModal"; +const IS_CLOUD = env("NEXT_PUBLIC_KAN_ENV") === "cloud"; +const HIDE_POWERED_BY = + env("NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY") === "true"; + export default function PublicBoardView() { const router = useRouter(); const { showPopup } = usePopup(); @@ -194,12 +199,26 @@ export default function PublicBoardView() { - - kan.bn - + {IS_CLOUD && ( + + kan.bn + + )} + + {!IS_CLOUD && !HIDE_POWERED_BY && ( + + {`Powered by`} + kan.bn + + )} diff --git a/docker-compose.yml b/docker-compose.yml index 89e54df0..3eddea0f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,6 +35,9 @@ services: - NEXT_PUBLIC_AVATAR_BUCKET_NAME=${NEXT_PUBLIC_AVATAR_BUCKET_NAME} - NEXT_PUBLIC_STORAGE_DOMAIN=${NEXT_PUBLIC_STORAGE_DOMAIN} + # White label + - NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY=${NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY} + # Auth config (optional) - NEXT_PUBLIC_ALLOW_CREDENTIALS=${NEXT_PUBLIC_ALLOW_CREDENTIALS} - NEXT_PUBLIC_DISABLE_SIGN_UP=${NEXT_PUBLIC_DISABLE_SIGN_UP} diff --git a/turbo.json b/turbo.json index 55da2f62..5e4c36ac 100644 --- a/turbo.json +++ b/turbo.json @@ -93,6 +93,7 @@ "SMTP_PASSWORD", "SMTP_SECURE", "NEXT_PUBLIC_KAN_ENV", + "NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY", "STRIPE_SECRET_KEY", "STRIPE_WEBHOOK_SECRET", "STRIPE_PRO_PLAN_PRICE_ID",