Compare commits
5 Commits
fix/kan-24
...
feat/laka-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8708c97937 | ||
|
|
042b57bfaa | ||
|
|
c6a26ee2ed | ||
|
|
d3c8cbd6c0 | ||
|
|
6c3dbaefd3 |
@@ -17,6 +17,7 @@ SMTP_USER=
|
|||||||
SMTP_PASSWORD=
|
SMTP_PASSWORD=
|
||||||
EMAIL_FROM= # e.g. "Kan <hello@mail.kan.bn>"
|
EMAIL_FROM= # e.g. "Kan <hello@mail.kan.bn>"
|
||||||
SMTP_SECURE= # set to "false" to use port 587
|
SMTP_SECURE= # set to "false" to use port 587
|
||||||
|
SMTP_REJECT_UNAUTHORIZED= # set to "false" to accept invalid certs
|
||||||
|
|
||||||
# Switch email features off entirely (optional)
|
# Switch email features off entirely (optional)
|
||||||
NEXT_PUBLIC_DISABLE_EMAIL=
|
NEXT_PUBLIC_DISABLE_EMAIL=
|
||||||
|
|||||||
75
README.md
75
README.md
@@ -34,7 +34,7 @@ See our [roadmap](https://kan.bn/kan/roadmap) for upcoming features.
|
|||||||
|
|
||||||
## Screenshot 👁️
|
## Screenshot 👁️
|
||||||
|
|
||||||
<img width="1507" alt="hero-dark" src="https://github.com/user-attachments/assets/5f7b6ad3-f31d-4b45-93dc-0132b3f2afd4" />
|
<img width="1507" alt="hero-dark" src="https://github.com/user-attachments/assets/8490104a-cd5d-49de-afc2-152fd8a93119" />
|
||||||
|
|
||||||
## Made With 🛠️
|
## Made With 🛠️
|
||||||
|
|
||||||
@@ -138,42 +138,43 @@ pnpm dev
|
|||||||
|
|
||||||
## Environment Variables 🔐
|
## Environment Variables 🔐
|
||||||
|
|
||||||
| Variable | Description | Required | Example |
|
| Variable | Description | Required | Example |
|
||||||
| ----------------------------------------- | -------------------------------------------------------- | ------------------------ | ----------------------------------------------------------- |
|
| ----------------------------------------- | --------------------------------------------------------- | ------------------------ | ----------------------------------------------------------- |
|
||||||
| `POSTGRES_URL` | PostgreSQL connection URL | To use external database | `postgres://user:pass@localhost:5432/db` |
|
| `POSTGRES_URL` | PostgreSQL connection URL | To use external database | `postgres://user:pass@localhost:5432/db` |
|
||||||
| `EMAIL_FROM` | Sender email address | For Email | `"Kan <hello@mail.kan.bn>"` |
|
| `EMAIL_FROM` | Sender email address | For Email | `"Kan <hello@mail.kan.bn>"` |
|
||||||
| `SMTP_HOST` | SMTP server hostname | For Email | `smtp.resend.com` |
|
| `SMTP_HOST` | SMTP server hostname | For Email | `smtp.resend.com` |
|
||||||
| `SMTP_PORT` | SMTP server port | For Email | `465` |
|
| `SMTP_PORT` | SMTP server port | For Email | `465` |
|
||||||
| `SMTP_USER` | SMTP username/email | No | `resend` |
|
| `SMTP_USER` | SMTP username/email | No | `resend` |
|
||||||
| `SMTP_PASSWORD` | SMTP password/token | No | `re_xxxx` |
|
| `SMTP_PASSWORD` | SMTP password/token | No | `re_xxxx` |
|
||||||
| `SMTP_SECURE` | Use secure SMTP connection (defaults to true if not set) | For Email | `true` |
|
| `SMTP_SECURE` | Use secure SMTP connection (defaults to true if not set) | For Email | `true` |
|
||||||
| `NEXT_PUBLIC_DISABLE_EMAIL` | To disable all email features | For Email | `true` |
|
| `SMTP_REJECT_UNAUTHORIZED` | Reject invalid certificates (defaults to true if not set) | For Email | `false` |
|
||||||
| `NEXT_PUBLIC_BASE_URL` | Base URL of your installation | Yes | `http://localhost:3000` |
|
| `NEXT_PUBLIC_DISABLE_EMAIL` | To disable all email features | For Email | `true` |
|
||||||
| `BETTER_AUTH_SECRET` | Auth encryption secret | Yes | Random 32+ char string |
|
| `NEXT_PUBLIC_BASE_URL` | Base URL of your installation | Yes | `http://localhost:3000` |
|
||||||
| `BETTER_AUTH_TRUSTED_ORIGINS` | Allowed callback origins | No | `http://localhost:3000,http://localhost:3001` |
|
| `BETTER_AUTH_SECRET` | Auth encryption secret | Yes | Random 32+ char string |
|
||||||
| `GOOGLE_CLIENT_ID` | Google OAuth client ID | For Google login | `xxx.apps.googleusercontent.com` |
|
| `BETTER_AUTH_TRUSTED_ORIGINS` | Allowed callback origins | No | `http://localhost:3000,http://localhost:3001` |
|
||||||
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret | For Google login | `xxx` |
|
| `GOOGLE_CLIENT_ID` | Google OAuth client ID | For Google login | `xxx.apps.googleusercontent.com` |
|
||||||
| `DISCORD_CLIENT_ID` | Discord OAuth client ID | For Discord login | `xxx` |
|
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret | For Google login | `xxx` |
|
||||||
| `DISCORD_CLIENT_SECRET` | Discord OAuth client secret | For Discord login | `xxx` |
|
| `DISCORD_CLIENT_ID` | Discord OAuth client ID | For Discord login | `xxx` |
|
||||||
| `GITHUB_CLIENT_ID` | GitHub OAuth client ID | For GitHub login | `xxx` |
|
| `DISCORD_CLIENT_SECRET` | Discord OAuth client secret | For Discord login | `xxx` |
|
||||||
| `GITHUB_CLIENT_SECRET` | GitHub OAuth client secret | For GitHub login | `xxx` |
|
| `GITHUB_CLIENT_ID` | GitHub OAuth client ID | For GitHub login | `xxx` |
|
||||||
| `OIDC_CLIENT_ID` | Generic OIDC client ID | For OIDC login | `xxx` |
|
| `GITHUB_CLIENT_SECRET` | GitHub OAuth client secret | For GitHub login | `xxx` |
|
||||||
| `OIDC_CLIENT_SECRET` | Generic OIDC client secret | For OIDC login | `xxx` |
|
| `OIDC_CLIENT_ID` | Generic OIDC client ID | For OIDC login | `xxx` |
|
||||||
| `OIDC_DISCOVERY_URL` | OIDC discovery URL | For OIDC login | `https://auth.example.com/.well-known/openid-configuration` |
|
| `OIDC_CLIENT_SECRET` | Generic OIDC client secret | For OIDC login | `xxx` |
|
||||||
| `TRELLO_APP_API_KEY` | Trello app API key | For Trello import | `xxx` |
|
| `OIDC_DISCOVERY_URL` | OIDC discovery URL | For OIDC login | `https://auth.example.com/.well-known/openid-configuration` |
|
||||||
| `TRELLO_APP_API_SECRET` | Trello app API secret | For Trello import | `xxx` |
|
| `TRELLO_APP_API_KEY` | Trello app API key | For Trello import | `xxx` |
|
||||||
| `S3_REGION` | S3 storage region | For file uploads | `WEUR` |
|
| `TRELLO_APP_API_SECRET` | Trello app API secret | For Trello import | `xxx` |
|
||||||
| `S3_ENDPOINT` | S3 endpoint URL | For file uploads | `https://xxx.r2.cloudflarestorage.com` |
|
| `S3_REGION` | S3 storage region | For file uploads | `WEUR` |
|
||||||
| `S3_ACCESS_KEY_ID` | S3 access key | For file uploads | `xxx` |
|
| `S3_ENDPOINT` | S3 endpoint URL | For file uploads | `https://xxx.r2.cloudflarestorage.com` |
|
||||||
| `S3_SECRET_ACCESS_KEY` | S3 secret key | For file uploads | `xxx` |
|
| `S3_ACCESS_KEY_ID` | S3 access key | For file uploads | `xxx` |
|
||||||
| `S3_FORCE_PATH_STYLE` | Use path-style URLs for S3 | For file uploads | `true` |
|
| `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` |
|
| `S3_FORCE_PATH_STYLE` | Use path-style URLs for S3 | For file uploads | `true` |
|
||||||
| `NEXT_PUBLIC_STORAGE_DOMAIN` | Storage domain name | For file uploads | `kanbn.com` |
|
| `NEXT_PUBLIC_STORAGE_URL` | Storage service URL | For file uploads | `https://storage.kanbn.com` |
|
||||||
| `NEXT_PUBLIC_AVATAR_BUCKET_NAME` | S3 bucket name for avatars | For file uploads | `avatars` |
|
| `NEXT_PUBLIC_STORAGE_DOMAIN` | Storage domain name | For file uploads | `kanbn.com` |
|
||||||
| `NEXT_PUBLIC_ATTATCHMENTS_BUCKET_NAME` | S3 bucket name for attatchments | For file uploads | `attatchments` |
|
| `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_ATTACHMENTS_BUCKET_NAME` | S3 bucket name for attachments | For file uploads | `attachments` |
|
||||||
| `NEXT_PUBLIC_DISABLE_SIGN_UP` | Disable sign up | For authentication | `false` |
|
| `NEXT_PUBLIC_ALLOW_CREDENTIALS` | Allow email & password login | For authentication | `true` |
|
||||||
| `NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY` | Hide “Powered by kan.bn” on public boards (self-host) | For white labelling | `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.
|
See `.env.example` for a complete list of supported environment variables.
|
||||||
|
|
||||||
|
|||||||
@@ -67,22 +67,23 @@ export function CardModal({
|
|||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<button
|
<button
|
||||||
className="absolute right-[2rem] top-[2rem] rounded p-1 hover:bg-light-300 focus:outline-none dark:hover:bg-dark-300"
|
className="absolute right-[2rem] top-[2rem] rounded p-1 hover:bg-light-300 focus:outline-none dark:hover:bg-dark-300"
|
||||||
onClick={async (e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
closeModal();
|
closeModal();
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(() => {
|
||||||
try {
|
void router.replace(
|
||||||
await router.replace(
|
{
|
||||||
`/${workspaceSlug}/${boardSlug}`,
|
pathname: router.pathname,
|
||||||
undefined,
|
query: {
|
||||||
{
|
...router.query,
|
||||||
shallow: true,
|
workspaceSlug,
|
||||||
|
boardSlug: [boardSlug],
|
||||||
},
|
},
|
||||||
);
|
},
|
||||||
} catch (error) {
|
undefined,
|
||||||
console.error(error);
|
{ shallow: true },
|
||||||
}
|
);
|
||||||
}, 400);
|
}, 400);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ export default function PublicBoardView() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const splitPath = router.asPath.split("/");
|
const pathWithoutQuery = router.asPath.split("?")[0];
|
||||||
|
const splitPath = pathWithoutQuery.split("/");
|
||||||
const cardPublicId = splitPath.length > 3 ? splitPath[3] : null;
|
const cardPublicId = splitPath.length > 3 ? splitPath[3] : null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -172,27 +173,36 @@ export default function PublicBoardView() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="scrollbar-track-rounded-[4px] scrollbar-thumb-rounded-[4px] scrollbar-w-[8px] z-10 h-full max-h-[calc(100vh-265px)] min-h-[2rem] overflow-y-auto pr-1 scrollbar dark:scrollbar-track-dark-100 dark:scrollbar-thumb-dark-600">
|
<div className="scrollbar-track-rounded-[4px] scrollbar-thumb-rounded-[4px] scrollbar-w-[8px] z-10 h-full max-h-[calc(100vh-265px)] min-h-[2rem] overflow-y-auto pr-1 scrollbar dark:scrollbar-track-dark-100 dark:scrollbar-thumb-dark-600">
|
||||||
{list.cards.map((card) => (
|
{list.cards.map((card) => {
|
||||||
<Link
|
return (
|
||||||
key={card.publicId}
|
<Link
|
||||||
href={`/${data.workspace.slug}/${data.slug}/${card.publicId}`}
|
key={card.publicId}
|
||||||
className={`mb-2 flex !cursor-pointer flex-col`}
|
href={{
|
||||||
shallow={true}
|
pathname: router.pathname,
|
||||||
onClick={() => {
|
query: {
|
||||||
openModal("CARD");
|
...router.query,
|
||||||
}}
|
workspaceSlug: data.workspace.slug,
|
||||||
>
|
boardSlug: [data.slug, card.publicId],
|
||||||
<Card
|
},
|
||||||
title={card.title}
|
}}
|
||||||
labels={card.labels}
|
className={`mb-2 flex !cursor-pointer flex-col`}
|
||||||
checklists={card.checklists ?? []}
|
shallow={true}
|
||||||
members={[]}
|
onClick={() => {
|
||||||
description={card.description}
|
openModal("CARD");
|
||||||
comments={card.comments ?? []}
|
}}
|
||||||
attachments={card.attachments}
|
>
|
||||||
/>
|
<Card
|
||||||
</Link>
|
title={card.title}
|
||||||
))}
|
labels={card.labels}
|
||||||
|
checklists={card.checklists ?? []}
|
||||||
|
members={[]}
|
||||||
|
description={card.description}
|
||||||
|
comments={card.comments ?? []}
|
||||||
|
attachments={card.attachments}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ services:
|
|||||||
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
||||||
- SMTP_SECURE=${SMTP_SECURE}
|
- SMTP_SECURE=${SMTP_SECURE}
|
||||||
- EMAIL_FROM=${EMAIL_FROM}
|
- EMAIL_FROM=${EMAIL_FROM}
|
||||||
|
- SMTP_REJECT_UNAUTHORIZED=${SMTP_REJECT_UNAUTHORIZED}
|
||||||
|
|
||||||
# Notifications
|
# Notifications
|
||||||
- NOVU_API_KEY=${NOVU_API_KEY}
|
- NOVU_API_KEY=${NOVU_API_KEY}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ services:
|
|||||||
- SMTP_USER=${SMTP_USER}
|
- SMTP_USER=${SMTP_USER}
|
||||||
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
||||||
- EMAIL_FROM=${EMAIL_FROM}
|
- EMAIL_FROM=${EMAIL_FROM}
|
||||||
|
- SMTP_REJECT_UNAUTHORIZED=${SMTP_REJECT_UNAUTHORIZED}
|
||||||
|
|
||||||
# Disable email features entirely (optional)
|
# Disable email features entirely (optional)
|
||||||
- NEXT_PUBLIC_DISABLE_EMAIL=${NEXT_PUBLIC_DISABLE_EMAIL}
|
- NEXT_PUBLIC_DISABLE_EMAIL=${NEXT_PUBLIC_DISABLE_EMAIL}
|
||||||
|
|||||||
@@ -16,7 +16,17 @@ const emailTemplates: Record<Templates, React.FC> = {
|
|||||||
const transporter = nodemailer.createTransport({
|
const transporter = nodemailer.createTransport({
|
||||||
host: process.env.SMTP_HOST,
|
host: process.env.SMTP_HOST,
|
||||||
port: Number(process.env.SMTP_PORT),
|
port: Number(process.env.SMTP_PORT),
|
||||||
secure: process.env.SMTP_SECURE !== "false",
|
secure:
|
||||||
|
process.env.SMTP_SECURE === undefined
|
||||||
|
? true
|
||||||
|
: process.env.SMTP_SECURE?.toLowerCase() === "true",
|
||||||
|
tls: {
|
||||||
|
// do not fail on invalid certs
|
||||||
|
rejectUnauthorized:
|
||||||
|
process.env.SMTP_REJECT_UNAUTHORIZED === undefined
|
||||||
|
? true
|
||||||
|
: process.env.SMTP_REJECT_UNAUTHORIZED?.toLowerCase() === "true",
|
||||||
|
},
|
||||||
...(process.env.SMTP_USER &&
|
...(process.env.SMTP_USER &&
|
||||||
process.env.SMTP_PASSWORD && {
|
process.env.SMTP_PASSWORD && {
|
||||||
auth: {
|
auth: {
|
||||||
|
|||||||
@@ -96,6 +96,7 @@
|
|||||||
"SMTP_USER",
|
"SMTP_USER",
|
||||||
"SMTP_PASSWORD",
|
"SMTP_PASSWORD",
|
||||||
"SMTP_SECURE",
|
"SMTP_SECURE",
|
||||||
|
"SMTP_REJECT_UNAUTHORIZED",
|
||||||
"NEXT_PUBLIC_KAN_ENV",
|
"NEXT_PUBLIC_KAN_ENV",
|
||||||
"NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY",
|
"NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY",
|
||||||
"STRIPE_SECRET_KEY",
|
"STRIPE_SECRET_KEY",
|
||||||
|
|||||||
Reference in New Issue
Block a user