Compare commits

...

7 Commits

Author SHA1 Message Date
Maksim Eltyshev
414418130d chore: Update version 2026-02-17 15:55:56 +01:00
Maksim Eltyshev
d83ea4b146 fix(terms): Display template notice, support custom terms loading
Closes #1523
2026-02-17 15:37:26 +01:00
Fabian Reinold
addad4378a feat(helm): Add image digest pinning support (#1531)
* feat(helm): add image digest field to values

- Add optional 'digest' field under image.repository configuration
- Allows users to pin container images by SHA256 digest
- Improves security through immutable image identification
- Fully backward compatible (digest is optional)

* feat(helm): implement image digest pinning in deployment template

- Add conditional logic to support SHA256 digest in image references
- When digest is set with tag: generates 'repository:tag@sha256:digest'
- When digest is set without tag: generates 'repository@sha256:digest'
- Preserves backward compatibility with tag-only deployments
- Validates tag presence to avoid invalid image references

* docs(helm): add image digest pinning documentation

- Add 'Image Digest Pinning' section under Advanced Configuration
- Include methods for finding image digests (docker inspect, skopeo)
- Document two usage options:
  - Option 1: Digest with tag (recommended) for reference + verification
  - Option 2: Digest only for minimalist configuration
- Explain security benefits (immutability, supply chain security, reproducibility)
- Provide complete helm and values.yaml examples
2026-02-16 14:25:47 +01:00
Maksim Eltyshev
b9967feeea fix(socket): Handle transport error during reconnection after idle 2026-02-16 10:50:09 +01:00
Maksim Eltyshev
dbcdd62bdf docs: Update readme 2026-02-12 16:03:53 +01:00
Maksim Eltyshev
ff4177f27a chore: Fix index view extension in ignore files 2026-02-11 23:00:45 +01:00
Maksim Eltyshev
f68e7d156e chore: Fix data path in docker-restore script 2026-02-11 22:58:40 +01:00
30 changed files with 235 additions and 75 deletions

View File

@@ -11,9 +11,13 @@ server/test
server/.tmp
server/.venv
server/views/index.ejs
server/views/index.html
server/data/*
!server/data/.gitkeep
server/terms/*
!server/terms/_template
!server/terms/cloud
client/dist

View File

@@ -69,7 +69,7 @@ jobs:
- name: Seed database with terms signature
run: |
TERMS_SIGNATURE=$(sha256sum terms/self-hosted/en-US.md | awk '{print $1}')
TERMS_SIGNATURE=$(sha256sum terms/_template/en-US.md | awk '{print $1}')
PGPASSWORD=$POSTGRES_PASSWORD psql -h localhost -U $POSTGRES_USERNAME -d $POSTGRES_DATABASE -c "UPDATE user_account SET terms_signature = '$TERMS_SIGNATURE';"
working-directory: ./server

View File

@@ -1,23 +1,27 @@
# PLANKA
<div align="center">
**Project mastering driven by fun**
![Logo](https://raw.githubusercontent.com/plankanban/planka/master/assets/logo.png)
![Version](https://img.shields.io/github/package-json/v/plankanban/planka?style=flat-square) [![Docker Pulls](https://img.shields.io/badge/docker_pulls-6M%2B-%23066da5?style=flat-square&color=red)](https://github.com/plankanban/planka/pkgs/container/planka) [![Contributors](https://img.shields.io/github/contributors/plankanban/planka?style=flat-square&color=blue)](https://github.com/plankanban/planka/graphs/contributors) [![Chat](https://img.shields.io/discord/1041440072953765979?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/WqqYNd7Jvt)
# PLANKA
![Demo](https://raw.githubusercontent.com/plankanban/planka/master/assets/demo.gif)
_Project mastering driven by fun_
[**Client demo**](https://plankanban.github.io/planka) (without server features).
![Version](https://img.shields.io/github/package-json/v/plankanban/planka?style=flat-square) [![Docker Pulls](https://img.shields.io/badge/docker_pulls-8M%2B-%23066da5?style=flat-square&color=red)](https://github.com/plankanban/planka/pkgs/container/planka) [![Contributors](https://img.shields.io/github/contributors/plankanban/planka?style=flat-square&color=blue)](https://github.com/plankanban/planka/graphs/contributors) [![Chat](https://img.shields.io/discord/1041440072953765979?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/WqqYNd7Jvt)
> ⚠️ The demo GIF and client demo are based on **v1** and will be updated soon.
[Install](https://docs.planka.cloud/docs/installation/docker/production-version/) · [Demo](https://planka.app) · [Docs](https://docs.planka.cloud/docs/welcome/) · [API](https://plankanban.github.io/planka/swagger-ui/) · [Cloud](https://planka.app/pricing) · [Pro version](https://planka.app/pro)
![Demo](https://raw.githubusercontent.com/plankanban/planka/master/assets/demo.gif)
</div>
## Key Features
- **Collaborative Kanban Boards**: Create projects, boards, lists, cards, and manage tasks with an intuitive drag-and-drop interface
- **Real-Time Updates**: Instant syncing across all users, no refresh needed
- **Rich Markdown Support**: Write beautifully formatted card descriptions with a powerful markdown editor
- **Flexible Notifications**: Get alerts through 100+ providers, fully customizable to your workflow
- **Seamless Authentication**: Single sign-on with OpenID Connect integration
- **Multilingual & Easy to Translate**: Full internationalization support for a global audience
- **Collaborative Kanban Boards:** Create projects, boards, lists, cards, and manage tasks with an intuitive drag-and-drop interface
- **Real-Time Updates:** Instant syncing across all users, no refresh needed
- **Rich Markdown Support:** Write beautifully formatted card descriptions with a powerful markdown editor
- **Flexible Notifications:** Get alerts through 100+ providers, fully customizable to your workflow
- **Seamless Authentication:** Single sign-on with OpenID Connect integration
- **Multilingual & Easy to Translate:** Full internationalization support for a global audience
## How to Deploy
@@ -25,20 +29,17 @@ PLANKA is easy to install using multiple methods - learn more in the [installati
For configuration and environment settings, see the [configuration section](https://docs.planka.cloud/docs/category/configuration/).
## Notes App Testing
Interested in a hosted or [Pro version](https://planka.app/pro) of PLANKA? Check out the pricing on our [website](https://planka.app/pricing).
The Notes app testing version is available across multiple platforms.
## Notes App
If you have an iOS device, you can join the TestFlight to try the app: [TestFlight](https://testflight.apple.com/join/5eJqTaJW).
A testing version of the Notes app is now available on multiple platforms:
For Windows and Android, you can find the app here: [PLANKA Notes](https://planka-notes.hillerdaniel.de).
> ⚠️ The Notes app has currently been tested only with PLANKA v2.
- **iOS:** Join the [TestFlight](https://testflight.apple.com/join/5eJqTaJW) to try the app
- **Windows & Android:** Download the app [here](https://planka-notes.hillerdaniel.de)
## Contact
Interested in a hosted version of PLANKA? Email us at [github@planka.group](mailto:github@planka.group).
For any security issues, please do not create a public issue on GitHub - instead, report it privately by emailing [security@planka.group](mailto:security@planka.group).
**Note:** We do NOT offer any public support via email, please use GitHub.
@@ -49,10 +50,10 @@ For any security issues, please do not create a public issue on GitHub - instead
PLANKA is [fair-code](https://faircode.io) distributed under the [Fair Use License](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Community%20License%20EN.md) and [PLANKA Pro/Enterprise License](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20Commercial%20License%20EN.md).
- **Source Available**: The source code is always visible
- **Self-Hostable**: Deploy and host it anywhere
- **Extensible**: Customize with your own functionality
- **Enterprise Licenses**: Available for additional features and support
- **Source Available:** The source code is always visible
- **Self-Hostable:** Deploy and host it anywhere
- **Extensible:** Customize with your own functionality
- **Enterprise Licenses:** Available for additional features and support
For more details, check the [License Guide](https://github.com/plankanban/planka/blob/master/LICENSES/PLANKA%20License%20Guide%20EN.md).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -15,13 +15,13 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 2.0.0
version: 2.0.1
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "2.0.0"
appVersion: "2.0.1"
dependencies:
- alias: postgresql

View File

@@ -68,7 +68,7 @@ helm install planka . --set secretkey=$SECRETKEY \
or create a values.yaml file like:
```yaml
````yaml
secretkey: "<InsertSecretKey>"
# The admin section needs to be present for new instances of PLANKA, after the first start you can remove the lines starting with admin_. If you want the admin user to be unchangeable admin_email: has to stay
# After changing the config you have to run ```helm upgrade planka . -f values.yaml```
@@ -89,11 +89,11 @@ ingress:
- path: /
pathType: ImplementationSpecific
# Needed for HTTPS
# Needed for HTTPS
tls:
- secretName: planka-tls # existing TLS secret in k8s
hosts:
- planka.example.dev
- secretName: planka-tls # existing TLS secret in k8s
hosts:
- planka.example.dev
```
```bash
@@ -135,14 +135,14 @@ extraMounts:
subPath: ca.crt
readOnly: true
configMap:
name: ca-certificates # Must exist
name: ca-certificates # Must exist
# Mount TLS certificates from existing Secret
- name: tls-certs
mountPath: /etc/ssl/private
readOnly: true
secret:
secretName: planka-tls-secret # Must exist
secretName: planka-tls-secret # Must exist
items:
- key: tls.crt
path: server.crt
@@ -178,11 +178,13 @@ extraMounts:
A common use case is configuring OIDC with a self-hosted Keycloak instance that uses custom CA certificates.
First, create the CA certificate ConfigMap:
```bash
kubectl create configmap ca-certificates --from-file=ca.crt=/path/to/your/ca.crt
```
Then configure the chart:
```yaml
# Mount custom CA certificate from existing ConfigMap
extraMounts:
@@ -225,6 +227,67 @@ extraEnv:
key: api-key
```
### Image Digest Pinning
For enhanced security and reproducibility, you can pin the container image using its SHA256 digest instead of relying solely on tags. This ensures you always deploy the exact same image, preventing tag mutations or accidental updates.
#### Finding the Image Digest
You can find the digest of a specific image tag using:
```bash
docker inspect ghcr.io/plankanban/planka:latest --format='{{index .RepoDigests 0}}'
# Output: ghcr.io/plankanban/planka@sha256:abc123def456...
# Or with skopeo
skopeo inspect docker://ghcr.io/plankanban/planka:latest
```
#### Usage
You can use digest pinning in several ways:
**Option 1: Digest with tag (recommended)**
Includes the tag for reference while using the digest for verification:
```bash
helm install planka . --set secretkey=$SECRETKEY \
--set image.tag=latest \
--set image.digest=abc123def456... \
--set admin_email="demo@demo.demo" \
--set admin_password="demo" \
--set admin_name="Demo Demo" \
--set admin_username="demo"
```
Or in values.yaml:
```yaml
image:
repository: ghcr.io/plankanban/planka
tag: latest
digest: "abc123def456ab89cd12ef34ab56cd78ef90ab12cd34ef56ab78cd90ef12ab34"
```
**Option 2: Digest only**
If you prefer to pin only by digest without specifying a tag:
```yaml
image:
repository: ghcr.io/plankanban/planka
tag: "" # Empty - digest alone identifies the image
digest: "abc123def456ab89cd12ef34ab56cd78ef90ab12cd34ef56ab78cd90ef12ab34"
```
#### Security Benefits
- **Immutability**: Ensures you always deploy the exact same image
- **Supply Chain Security**: Protects against tag mutations or registry compromise
- **Reproducibility**: Makes deployments fully reproducible across environments
- **Audit Trail**: Provides clear image identity in deployment manifests
### Complete Example
See `values-example.yaml` for a comprehensive example that demonstrates all the advanced features including OIDC configuration with custom CA certificates.

View File

@@ -39,7 +39,16 @@ spec:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
{{- $imageTag := .Values.image.tag | default .Chart.AppVersion }}
{{- if .Values.image.digest }}
{{- if $imageTag }}
image: "{{ .Values.image.repository }}:{{ $imageTag }}@sha256:{{ .Values.image.digest }}"
{{- else }}
image: "{{ .Values.image.repository }}@sha256:{{ .Values.image.digest }}"
{{- end }}
{{- else }}
image: "{{ .Values.image.repository }}:{{ $imageTag }}"
{{- end }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http

View File

@@ -9,6 +9,10 @@ image:
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: ""
# Optional: specify the image digest for pinning by SHA256
# When set, the image reference will include the digest for enhanced security
# Example: "abc123def456..." (without sha256: prefix)
digest: ""
imagePullSecrets: []
nameOverride: ""

View File

@@ -30,8 +30,8 @@ socket.connect = socket._connect; // eslint-disable-line no-underscore-dangle
headers,
url: `/api${url}`,
},
(_, { body, error }) => {
if (error) {
(body, { error }) => {
if (body instanceof Error || error) {
reject(body);
} else {
resolve(body);

View File

@@ -1,3 +1,10 @@
# [2.0.1] - 2026-02-17
### Fixed
* Improve connection reliability after the app is idle
* Allow loading custom End User Terms of Service
## [2.0.0] - 2026-02-11
### Added

View File

@@ -11,15 +11,12 @@ import { Button, Checkbox, Dropdown, Modal, Segment } from 'semantic-ui-react';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
import { localeByLanguage } from '../../../locales';
import TERMS_LANGUAGES from '../../../constants/TermsLanguages';
import Markdown from '../Markdown';
import styles from './TermsModal.module.scss';
const LOCALES = TERMS_LANGUAGES.map((language) => localeByLanguage[language]);
const splitTermsAndConfirmations = (content) => {
const separator = '\n---\n';
const separator = '\n[confirmations]::\n---\n';
const index = content.lastIndexOf(separator);
if (index === -1) {
@@ -38,6 +35,8 @@ const splitTermsAndConfirmations = (content) => {
};
const TermsModal = React.memo(() => {
const { termsLanguages } = useSelector(selectors.selectBootstrap);
const {
termsForm: { payload: terms, isSubmitting, isCancelling, isLanguageUpdating },
} = useSelector(selectors.selectAuthenticateForm);
@@ -46,6 +45,19 @@ const TermsModal = React.memo(() => {
const [t] = useTranslation();
const [acceptedConfirmationsSet, setAcceptedConfirmationsSet] = useState(new Set());
const locales = useMemo(
() =>
termsLanguages.map(
(language) =>
localeByLanguage[language] || {
language,
country: language.split('-')[1]?.toLowerCase(),
name: language,
},
),
[termsLanguages],
);
const [content, confirmations] = useMemo(
() => splitTermsAndConfirmations(terms.content),
[terms.content],
@@ -88,7 +100,7 @@ const TermsModal = React.memo(() => {
<Dropdown
fluid
selection
options={LOCALES.map((locale) => ({
options={locales.map((locale) => ({
value: locale.language,
flag: locale.country,
text: locale.name,

View File

@@ -1,6 +0,0 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
export default ['de-DE', 'en-US'];

View File

@@ -1 +1 @@
export default '2.0.0';
export default '2.0.1';

View File

@@ -4,6 +4,7 @@ services:
restart: on-failure
volumes:
- data:/app/data
# - ./terms:/app/terms/custom
# Optionally override this to your user/group
# user: 1000:1000
# tmpfs:

View File

@@ -21,7 +21,7 @@ echo "Success!"
# Restore Docker Volume
echo -n "Importing data volume ... "
docker run --rm --volumes-from "$PLANKA_DOCKER_CONTAINER_PLANKA" -v "$(pwd)/$PLANKA_BACKUP_ARCHIVE:/backup" ubuntu cp -rf /backup/data/. /app/public/data
docker run --rm --volumes-from "$PLANKA_DOCKER_CONTAINER_PLANKA" -v "$(pwd)/$PLANKA_BACKUP_ARCHIVE:/backup" ubuntu cp -rf /backup/data/. /app/data
echo "Success!"
echo -n "Cleaning up temporary files and folders ... "

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "planka",
"version": "2.0.0",
"version": "2.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "planka",
"version": "2.0.0",
"version": "2.0.1",
"hasInstallScript": true,
"dependencies": {
"concurrently": "^9.2.1",

View File

@@ -1,6 +1,6 @@
{
"name": "planka",
"version": "2.0.0",
"version": "2.0.1",
"private": true,
"scripts": {
"client:build": "npm run build --prefix client",

View File

@@ -20,6 +20,10 @@ test
.tmp
.venv
views/index.ejs
views/index.html
data/*
terms/*
!terms/_template
!terms/cloud

6
server/.gitignore vendored
View File

@@ -134,7 +134,11 @@ swagger.json
dist
logs
views/index.ejs
views/index.html
data/*
!data/.gitkeep
terms/*
!terms/_template
!terms/cloud

View File

@@ -57,6 +57,12 @@
* format: uri
* description: URL to the customer management panel (conditionally added for admins if configured)
* example: https://panel.example.com
* termsLanguages:
* type: array
* description: List of available language codes for terms localization
* items:
* type: string
* example: [de-DE, en-US]
* version:
* type: string
* description: Current version of the PLANKA application

View File

@@ -19,7 +19,6 @@
* description: Language code for terms localization
* schema:
* type: string
* enum: [de-DE, en-US]
* example: en-US
* responses:
* 200:
@@ -41,7 +40,6 @@
* properties:
* language:
* type: string
* enum: [de-DE, en-US]
* description: Language code used
* example: en-US
* content:
@@ -65,7 +63,6 @@ module.exports = {
inputs: {
language: {
type: 'string',
isIn: User.LANGUAGES,
},
},

View File

@@ -22,6 +22,7 @@ module.exports = {
fn(inputs) {
const data = {
oidc: inputs.oidc,
termsLanguages: sails.hooks.terms.getLanguages(),
version: sails.config.custom.version,
};

View File

@@ -12,25 +12,35 @@
*/
const fsPromises = require('fs').promises;
const path = require('path');
const crypto = require('crypto');
const LANGUAGES = ['de-DE', 'en-US'];
const DEFAULT_LANGUAGE = 'en-US';
const getContent = (language = DEFAULT_LANGUAGE) =>
fsPromises.readFile(
`${sails.config.appPath}/terms/${sails.config.custom.termsType}/${language}.md`,
'utf8',
);
const PATH = path.join(sails.config.appPath, 'terms');
const TEMPLATE_TYPE = '_template';
const hashContent = (content) => crypto.createHash('sha256').update(content).digest('hex');
module.exports = function defineTermsHook(sails) {
let type;
let languages;
let defaultLanguage;
let signature;
return {
LANGUAGES,
const getLanguages = async () => {
const entries = await fsPromises.readdir(path.join(PATH, type), {
withFileTypes: true,
});
return entries
.filter((entry) => entry.isFile() && path.extname(entry.name) === '.md')
.map((entry) => path.basename(entry.name, '.md'))
.sort();
};
const getContent = (language) =>
fsPromises.readFile(path.join(PATH, type, `${language}.md`), 'utf8');
return {
/**
* Runs when this Sails app loads/lifts.
*/
@@ -38,13 +48,32 @@ module.exports = function defineTermsHook(sails) {
async initialize() {
sails.log.info('Initializing custom hook (`terms`)');
const content = await getContent();
type = sails.config.custom.termsType;
try {
languages = await getLanguages();
} catch (error) {
/* empty */
}
if (!languages || languages.length === 0) {
sails.log.warn('Custom terms not found, falling back to template');
type = TEMPLATE_TYPE;
languages = await getLanguages();
}
defaultLanguage = languages.includes(sails.config.i18n.defaultLocale)
? sails.config.i18n.defaultLocale
: languages[0];
const content = await getContent(defaultLanguage);
signature = hashContent(content);
},
async getPayload(language = DEFAULT_LANGUAGE) {
if (!LANGUAGES.includes(language)) {
language = DEFAULT_LANGUAGE; // eslint-disable-line no-param-reassign
async getPayload(language) {
if (!language || !languages.includes(language)) {
language = defaultLanguage; // eslint-disable-line no-param-reassign
}
const content = await getContent(language);
@@ -56,6 +85,10 @@ module.exports = function defineTermsHook(sails) {
};
},
getLanguages() {
return languages;
},
isSignatureValid(value) {
return value === signature;
},

View File

@@ -113,7 +113,7 @@ module.exports.custom = {
/* Internal */
internalAccessToken: process.env.INTERNAL_ACCESS_TOKEN,
termsType: process.env.TERMS_TYPE || 'self-hosted',
termsType: process.env.TERMS_TYPE || 'custom',
customerPanelUrl: process.env.CUSTOMER_PANEL_URL,
demoMode: process.env.DEMO_MODE === 'true',
};

View File

@@ -1,3 +1,11 @@
# ⚠️ DIES IST NUR EINE BEISPIEL-VORLAGE
Wenn Sie Administrator dieser Instanz sind, können Sie diese Bedingungen an Ihre eigenen Bedürfnisse und rechtlichen Anforderungen anpassen.
Eine Anleitung zum Anpassen dieser Vorlage finden Sie in diesem [Guide](https://docs.planka.cloud/docs/configuration/customizing-end-user-terms/).
---
# Nutzungsbedingungen für Endbenutzer On-Premise-Version
**Stand: 11. Februar 2026 v1.0**
@@ -76,6 +84,7 @@ Der Anbieter kann diese Nutzungsbedingungen mit Wirkung für die Zukunft ändern
*PLANKA Software GmbH · Lindauer Str. 4 · 87439 Kempten · Deutschland*
[confirmations]::
---
✔️ **Ich habe diese Nutzungsbedingungen gelesen und akzeptiere sie**

View File

@@ -1,3 +1,11 @@
# ⚠️ THIS IS ONLY A TEMPLATE
If you are the admin of this instance, you can customize these Terms to suit your own needs and legal requirements.
For guidance on updating this template, see this [guide](https://docs.planka.cloud/docs/configuration/customizing-end-user-terms/).
---
# End User Terms of Service On-Premise Version
**Effective: February 11, 2026 v1.0**
@@ -76,6 +84,7 @@ The Provider may amend these End User Terms of Service with effect for the futur
*PLANKA Software GmbH · Lindauer Str. 4 · 87439 Kempten · Germany*
[confirmations]::
---
✔️ **I have read and accept these End User Terms of Service**

View File

@@ -124,6 +124,7 @@ Die vollständige Datenschutzerklärung mit allen Details zu Sub-Auftragsverarbe
*PLANKA Software GmbH · Lindauer Str. 4 · 87439 Kempten · Deutschland*
[confirmations]::
---
✔️ **Ich habe die Nutzungsbedingungen (Teil I) gelesen und akzeptiere sie**

View File

@@ -124,6 +124,7 @@ The full Privacy Policy with complete details on sub-processors, technical measu
*PLANKA Software GmbH · Lindauer Str. 4 · 87439 Kempten · Germany*
[confirmations]::
---
✔️ **I have read and accept the Terms of Service (Part I)**

View File

@@ -1 +1 @@
module.exports = '2.0.0';
module.exports = '2.0.1';