1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-18 20:59:44 +02:00

Merge branch 'plankanban:master' into master

This commit is contained in:
P1ng140 2025-01-02 13:53:26 +02:00 committed by GitHub
commit b822bc8a77
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 3206 additions and 4456 deletions

View file

@ -1,4 +1,4 @@
FROM node:18-alpine as server-dependencies
FROM node:18-alpine AS server-dependencies
RUN apk -U upgrade \
&& apk add build-base python3 \
@ -8,7 +8,7 @@ WORKDIR /app
COPY server/package.json server/package-lock.json ./
RUN npm install npm@latest --global \
RUN npm install npm --global \
&& npm install pnpm --global \
&& pnpm import \
&& pnpm install --prod
@ -19,7 +19,7 @@ WORKDIR /app
COPY client/package.json client/package-lock.json ./
RUN npm install npm@latest --global \
RUN npm install npm --global \
&& npm install pnpm --global \
&& pnpm import \
&& pnpm install --prod

View file

@ -1,7 +1,7 @@
# Planka
#### Elegant open source project tracking.
![David (path)](https://img.shields.io/github/package-json/v/plankanban/planka) ![Docker Pulls](https://img.shields.io/badge/docker_pulls-4M%2B-%23066da5) ![GitHub](https://img.shields.io/github/license/plankanban/planka)
![David (path)](https://img.shields.io/github/package-json/v/plankanban/planka) ![Docker Pulls](https://img.shields.io/badge/docker_pulls-5M%2B-%23066da5) ![GitHub](https://img.shields.io/github/license/plankanban/planka)
![](https://raw.githubusercontent.com/plankanban/planka/master/demo.gif)

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: 0.2.15
version: 0.2.19
# 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: "1.24.0"
appVersion: "1.24.3"
dependencies:
- alias: postgresql

View file

@ -67,6 +67,20 @@ spec:
resources:
{{- toYaml .Values.resources | nindent 12 }}
env:
{{- if .Values.extraEnv }}
{{- range .Values.extraEnv }}
- name: {{ .name }}
{{- if .value }}
value: {{ .value | quote}}
{{- end }}
{{- if .valueFrom }}
valueFrom:
secretKeyRef:
name: {{ .valueFrom.secretName }}
key: {{ .valueFrom.key }}
{{- end }}
{{- end }}
{{- end }}
{{- if not .Values.postgresql.enabled }}
{{- if .Values.existingDburlSecret }}
- name: DATABASE_URL

View file

@ -69,7 +69,7 @@ ingress:
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
# Used to set planka BASE_URL if no `baseurl` is provided.
# Used to set planka BASE_URL if no `baseurl` is provided.
- host: planka.local
paths:
- path: /
@ -197,3 +197,16 @@ oidc:
##
roles: []
# - planka-admin
## Extra environment variables for planka deployment
## Supports hard coded and getting values from a k8s secret
## - name: test
## value: valuetest
## - name: another
## value: another
## - name: test-secret
## valueFrom:
## secretName: k8s-secret-name
## key: key-inside-the-secret
##
extraEnv: []

4856
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -66,15 +66,15 @@
"dequal": "^2.0.3",
"easymde": "^2.18.0",
"history": "^5.3.0",
"i18next": "^23.15.1",
"i18next": "23.15.2",
"i18next-browser-languagedetector": "^8.0.0",
"initials": "^3.1.2",
"js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0",
"linkify-react": "^4.1.3",
"linkifyjs": "^4.1.3",
"linkify-react": "^4.1.4",
"linkifyjs": "^4.1.4",
"lodash": "^4.17.21",
"nanoid": "^5.0.7",
"nanoid": "^5.0.8",
"node-sass": "^9.0.0",
"photoswipe": "^5.4.4",
"prop-types": "^15.8.1",
@ -83,16 +83,16 @@
"react-beautiful-dnd": "^13.1.1",
"react-datepicker": "^4.25.0",
"react-dom": "18.2.0",
"react-dropzone": "^14.2.3",
"react-i18next": "^15.0.2",
"react-dropzone": "^14.3.5",
"react-i18next": "^15.1.1",
"react-input-mask": "^2.0.4",
"react-markdown": "^8.0.7",
"react-photoswipe-gallery": "^2.2.7",
"react-redux": "^8.1.3",
"react-router-dom": "^6.26.2",
"react-router-dom": "^6.28.0",
"react-scripts": "5.0.1",
"react-simplemde-editor": "^5.2.0",
"react-textarea-autosize": "^8.5.3",
"react-textarea-autosize": "^8.5.5",
"redux": "^4.2.1",
"redux-logger": "^3.0.6",
"redux-orm": "^0.16.2",
@ -109,22 +109,22 @@
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@cucumber/cucumber": "^7.3.1",
"@cucumber/cucumber": "^7.3.2",
"@cucumber/pretty-formatter": "^1.0.1",
"@playwright/test": "^1.46.1",
"@testing-library/jest-dom": "^6.5.0",
"@playwright/test": "^1.49.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^15.0.7",
"@testing-library/user-event": "^14.5.2",
"axios": "^1.6.2",
"babel-preset-airbnb": "^5.0.0",
"chai": "^4.5.0",
"eslint": "^8.57.0",
"eslint": "8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-jsx-a11y": "^6.10.0",
"eslint-plugin-react": "^7.36.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^4.6.2",
"playwright": "^1.46.1",
"playwright": "^1.49.0",
"react-test-renderer": "18.2.0"
}
}

View file

@ -9,7 +9,7 @@
name="description"
content="Planka is an open source project management software"
/>
<link rel="apple-touch-icon" href="logo192.png" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/

View file

@ -11,6 +11,7 @@ import ListAdd from './ListAdd';
import { ReactComponent as PlusMathIcon } from '../../assets/images/plus-math-icon.svg';
import styles from './Board.module.scss';
import globalStyles from '../../styles.module.scss';
const parseDndId = (dndId) => dndId.split(':')[1];
@ -31,11 +32,14 @@ const Board = React.memo(
}, []);
const handleDragStart = useCallback(() => {
document.body.classList.add(globalStyles.dragging);
closePopup();
}, []);
const handleDragEnd = useCallback(
({ draggableId, type, source, destination }) => {
document.body.classList.remove(globalStyles.dragging);
if (
!destination ||
(source.droppableId === destination.droppableId && source.index === destination.index)
@ -72,13 +76,16 @@ const Board = React.memo(
}
prevPosition.current = event.clientX;
window.getSelection().removeAllRanges();
document.body.classList.add(globalStyles.dragScrolling);
},
[wrapper],
);
const handleWindowMouseMove = useCallback(
(event) => {
if (!prevPosition.current) {
if (prevPosition.current === null) {
return;
}
@ -93,8 +100,13 @@ const Board = React.memo(
[prevPosition],
);
const handleWindowMouseUp = useCallback(() => {
const handleWindowMouseRelease = useCallback(() => {
if (prevPosition.current === null) {
return;
}
prevPosition.current = null;
document.body.classList.remove(globalStyles.dragScrolling);
}, [prevPosition]);
useEffect(() => {
@ -112,14 +124,20 @@ const Board = React.memo(
}, [listIds, isListAddOpened]);
useEffect(() => {
window.addEventListener('mouseup', handleWindowMouseUp);
window.addEventListener('mousemove', handleWindowMouseMove);
window.addEventListener('mouseup', handleWindowMouseRelease);
window.addEventListener('blur', handleWindowMouseRelease);
window.addEventListener('contextmenu', handleWindowMouseRelease);
return () => {
window.removeEventListener('mouseup', handleWindowMouseUp);
window.removeEventListener('mousemove', handleWindowMouseMove);
window.removeEventListener('mouseup', handleWindowMouseRelease);
window.removeEventListener('blur', handleWindowMouseRelease);
window.removeEventListener('contextmenu', handleWindowMouseRelease);
};
}, [handleWindowMouseUp, handleWindowMouseMove]);
}, [handleWindowMouseMove, handleWindowMouseRelease]);
return (
<>

View file

@ -13,6 +13,7 @@ import AddStep from './AddStep';
import EditStep from './EditStep';
import styles from './Boards.module.scss';
import globalStyles from '../../styles.module.scss';
const Boards = React.memo(({ items, currentId, canEdit, onCreate, onUpdate, onMove, onDelete }) => {
const tabsWrapper = useRef(null);
@ -24,11 +25,14 @@ const Boards = React.memo(({ items, currentId, canEdit, onCreate, onUpdate, onMo
}, []);
const handleDragStart = useCallback(() => {
document.body.classList.add(globalStyles.dragging);
closePopup();
}, []);
const handleDragEnd = useCallback(
({ draggableId, source, destination }) => {
document.body.classList.remove(globalStyles.dragging);
if (!destination || source.index === destination.index) {
return;
}

View file

@ -44,7 +44,7 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate },
);
const handleChildrenClick = useCallback(() => {
if (!getSelection().toString()) {
if (!window.getSelection().toString()) {
open();
}
}, [open]);

View file

@ -10,16 +10,20 @@ import Item from './Item';
import Add from './Add';
import styles from './Tasks.module.scss';
import globalStyles from '../../../styles.module.scss';
const Tasks = React.memo(({ items, canEdit, onCreate, onUpdate, onMove, onDelete }) => {
const [t] = useTranslation();
const handleDragStart = useCallback(() => {
document.body.classList.add(globalStyles.dragging);
closePopup();
}, []);
const handleDragEnd = useCallback(
({ draggableId, source, destination }) => {
document.body.classList.remove(globalStyles.dragging);
if (!destination || source.index === destination.index) {
return;
}

View file

@ -67,11 +67,18 @@ const DueDateEditStep = React.memo(({ defaultValue, onUpdate, onBack, onClose })
return;
}
const value = parseTime(data.time, nullableDate);
let value = t('format:dateTime', {
postProcess: 'parseDate',
value: `${data.date} ${data.time}`,
});
if (Number.isNaN(value.getTime())) {
timeField.current.select();
return;
value = parseTime(data.time, nullableDate);
if (Number.isNaN(value.getTime())) {
timeField.current.select();
return;
}
}
if (!defaultValue || value.getTime() !== defaultValue.getTime()) {
@ -79,7 +86,7 @@ const DueDateEditStep = React.memo(({ defaultValue, onUpdate, onBack, onClose })
}
onClose();
}, [defaultValue, onUpdate, onClose, data, nullableDate]);
}, [defaultValue, onUpdate, onClose, data, nullableDate, t]);
const handleClearClick = useCallback(() => {
if (defaultValue) {

View file

@ -13,6 +13,7 @@ import EditStep from './EditStep';
import Item from './Item';
import styles from './LabelsStep.module.scss';
import globalStyles from '../../styles.module.scss';
const StepTypes = {
ADD: 'ADD',
@ -77,8 +78,14 @@ const LabelsStep = React.memo(
[onDeselect],
);
const handleDragStart = useCallback(() => {
document.body.classList.add(globalStyles.dragging);
}, []);
const handleDragEnd = useCallback(
({ draggableId, source, destination }) => {
document.body.classList.remove(globalStyles.dragging);
if (!destination || source.index === destination.index) {
return;
}
@ -159,7 +166,7 @@ const LabelsStep = React.memo(
onChange={handleSearchChange}
/>
{filteredItems.length > 0 && (
<DragDropContext onDragEnd={handleDragEnd}>
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<Droppable droppableId="labels" type={DroppableTypes.LABEL}>
{({ innerRef, droppableProps, placeholder }) => (
<div

View file

@ -32,7 +32,7 @@ const List = React.memo(
const [isAddCardOpened, setIsAddCardOpened] = useState(false);
const nameEdit = useRef(null);
const listWrapper = useRef(null);
const cardsWrapper = useRef(null);
const handleHeaderClick = useCallback(() => {
if (isPersisted && canEdit) {
@ -67,7 +67,7 @@ const List = React.memo(
useEffect(() => {
if (isAddCardOpened) {
listWrapper.current.scrollTop = listWrapper.current.scrollHeight;
cardsWrapper.current.scrollTop = cardsWrapper.current.scrollHeight;
}
}, [cardIds, isAddCardOpened]);
@ -133,13 +133,7 @@ const List = React.memo(
</ActionsPopup>
)}
</div>
<div
ref={listWrapper}
className={classNames(
styles.cardsInnerWrapper,
(isAddCardOpened || !canEdit) && styles.cardsInnerWrapperFull,
)}
>
<div ref={cardsWrapper} className={styles.cardsInnerWrapper}>
<div className={styles.cardsOuterWrapper}>{cardsNode}</div>
</div>
{!isAddCardOpened && canEdit && (

View file

@ -40,7 +40,6 @@
}
.cardsInnerWrapper {
max-height: calc(100vh - 268px);
overflow-x: hidden;
overflow-y: auto;
width: 290px;
@ -62,10 +61,6 @@
}
}
.cardsInnerWrapperFull {
max-height: calc(100vh - 232px);
}
.cardsOuterWrapper {
padding: 0 8px;
white-space: normal;
@ -140,6 +135,9 @@
.outerWrapper {
background: #dfe3e6;
border-radius: 3px;
display: flex;
flex-direction: column;
max-height: calc(100vh - 198px);
overflow: hidden;
}
}

View file

@ -19,6 +19,8 @@ import ptBR from './pt-BR';
import roRO from './ro-RO';
import ruRU from './ru-RU';
import skSK from './sk-SK';
import srCyrlCS from './sr-Cyrl-CS';
import srLatnCS from './sr-Latn-CS';
import svSE from './sv-SE';
import trTR from './tr-TR';
import ukUA from './uk-UA';
@ -48,6 +50,8 @@ const locales = [
roRO,
ruRU,
skSK,
srCyrlCS,
srLatnCS,
svSE,
trTR,
ukUA,

View file

@ -3,6 +3,7 @@ export default {
common: {
emailOrUsername: 'E-mail или имя пользователя',
invalidEmailOrUsername: 'Неверный e-mail или имя пользователя',
invalidCredentials: 'Недействительные учетные данные',
invalidPassword: 'Неверный пароль',
logInToPlanka: 'Вход в Planka',
noInternetConnection: 'Нет соединения',

View file

@ -0,0 +1,253 @@
import dateFns from 'date-fns/locale/sr';
export default {
dateFns,
format: {
date: 'd.M.yyyy.',
time: 'p',
dateTime: '$t(format:date) $t(format:time)',
longDate: 'd. MMM',
longDateTime: "d. MMMM 'u' p",
fullDate: 'd. MMM y',
fullDateTime: "d. MMMM y 'u' p",
},
translation: {
common: {
aboutPlanka: 'O Planka',
account: 'Налог',
actions: 'Радње',
addAttachment_title: 'Додај прилог',
addComment: 'Додај коментар',
addManager_title: 'Додај руководиоца',
addMember_title: 'Додај члана',
addUser_title: 'Додај корисника',
administrator: 'Администратор',
all: 'Све',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Све промене ће аутоматски бити сачуване<br />након успостављања конекције.',
areYouSureYouWantToDeleteThisAttachment: 'Да ли заиста желите да обришете овај прилог?',
areYouSureYouWantToDeleteThisBoard: 'Да ли заиста желите да обришете ову таблу?',
areYouSureYouWantToDeleteThisCard: 'Да ли заиста желите да обришете ову картицу?',
areYouSureYouWantToDeleteThisComment: 'Да ли заиста желите да обришете овај коментар?',
areYouSureYouWantToDeleteThisLabel: 'Да ли заиста желите да обришете ову ознаку?',
areYouSureYouWantToDeleteThisList: 'Да ли заиста желите да обришете овај списак?',
areYouSureYouWantToDeleteThisProject: 'Да ли заиста желите да обришете овај пројекат?',
areYouSureYouWantToDeleteThisTask: 'Да ли заиста желите да обришете овај задатак?',
areYouSureYouWantToDeleteThisUser: 'Да ли заиста желите да обришете овог корисника?',
areYouSureYouWantToLeaveBoard: 'Да ли заиста желите да напустите ову таблу?',
areYouSureYouWantToLeaveProject: 'Да ли заиста желите да напустите овај пројекат?',
areYouSureYouWantToRemoveThisManagerFromProject:
'Да ли заиста желите да уклоните овог руководиоца из овог пројекта?',
areYouSureYouWantToRemoveThisMemberFromBoard:
'Да ли заиста желите да уклоните овог члана из ове табле?',
attachment: 'Прилог',
attachments: 'Прилози',
authentication: 'Аутентификација',
background: 'Позадина',
board: 'Табла',
boardNotFound_title: 'Табла није пронађена',
canComment: 'Може да коментарише',
canEditContentOfBoard: 'Може да уређује садржај табле.',
canOnlyViewBoard: 'Може само да прегледа таблу.',
cardActions_title: 'Радње над картицом',
cardNotFound_title: 'Картица није пронађена',
cardOrActionAreDeleted: 'Картица или радња су обрисане.',
color: 'Боја',
copy_inline: 'копија',
createBoard_title: 'Направи таблу',
createLabel_title: 'Направи ознаку',
createNewOneOrSelectExistingOne: 'Направи нову или изабери<br />постојећу.',
createProject_title: 'Направи пројекат',
createTextFile_title: 'Направи текстуалну датотеку',
currentPassword: 'Тренутна лозинка',
dangerZone_title: 'Опасна зона',
date: 'Датум',
dueDate: 'Рок',
dueDate_title: 'Рок',
deleteAttachment_title: 'Обриши прилог',
deleteBoard_title: 'Обриши таблу',
deleteCard_title: 'Обриши картицу',
deleteComment_title: 'Обриши коментар',
deleteLabel_title: 'Обриши ознаку',
deleteList_title: 'Обриши списак',
deleteProject_title: 'Обриши пројекат',
deleteTask_title: 'Обриши задатак',
deleteUser_title: 'Обриши корисника',
description: 'Опис',
detectAutomatically: 'Детектуј аутоматски',
dropFileToUpload: 'Превуци датотеку за слање',
editor: 'Уређивач',
editAttachment_title: 'Уреди прилог',
editAvatar_title: 'Уреди аватара',
editBoard_title: 'Уреди таблу',
editDueDate_title: 'Уреди рок',
editEmail_title: 'Уреди е-пошту',
editInformation_title: 'Уреди информације',
editLabel_title: 'Уреди ознаку',
editPassword_title: 'Измени лозинку',
editPermissions_title: 'Уреди овлашћења',
editStopwatch_title: 'Уреди штоперицу',
editUsername_title: 'Измени корисничко име',
email: 'Е-пошта',
emailAlreadyInUse: 'Е-пошта је већ у употреби',
enterCardTitle: 'Унеси наслов картице... [Ctrl+Enter] да се аутоматски отвори.',
enterDescription: 'Унеси опис...',
enterFilename: 'Унеси назив датотеке',
enterListTitle: 'Унеси наслов списка...',
enterProjectTitle: 'Унеси наслов пројекта',
enterTaskDescription: 'Унеси опис задатка...',
filterByLabels_title: 'Филтрирај према ознакама',
filterByMembers_title: 'Филтрирај према члановима',
fromComputer_title: 'Са рачунара',
fromTrello: 'Са Trello-а',
general: 'Опште',
hours: 'Сати',
importBoard_title: 'Увези таблу',
invalidCurrentPassword: 'Неисправна тренутна лозинка',
labels: 'Ознаке',
language: 'Језик',
leaveBoard_title: 'Напусти таблу',
leaveProject_title: 'Напусти пројекат',
linkIsCopied: 'Веза је ископирана',
list: 'Списак',
listActions_title: 'Радње над списком',
managers: 'Руководиоци',
managerActions_title: 'Радње над руководиоцима',
members: 'Чланови',
memberActions_title: 'Радње над члановима',
minutes: 'Минути',
moveCard_title: 'Премести картицу',
name: 'Име',
newestFirst: 'Прво најновије',
newEmail: 'Нова е-пошта',
newPassword: 'Нова лозинка',
newUsername: 'Ново корисничко име',
noConnectionToServer: 'Нема конекције са сервером',
noBoards: 'Нема табли',
noLists: 'Нема спискова',
noProjects: 'Нема пројеката',
notifications: 'Обавештења',
noUnreadNotifications: 'Нема непрочитаних обавештења.',
oldestFirst: 'Прво најстарије',
openBoard_title: 'Отвори таблу',
optional_inline: 'опционо',
organization: 'Организација',
phone: 'Телефон',
preferences: 'Својства',
pressPasteShortcutToAddAttachmentFromClipboard:
'Савет: притисни Ctrl-V (Cmd-V на Меку) да би додао прилог са бележнице.',
project: 'Пројекат',
projectNotFound_title: 'Пројекат није пронађен',
removeManager_title: 'Уклони руководиоца',
removeMember_title: 'Уклони члана',
searchLabels: 'Претражи ознаке...',
searchMembers: 'Претражи чланове...',
searchUsers: 'Претражи кориснике...',
searchCards: 'Претражи картице...',
seconds: 'Секунде',
selectBoard: 'Изабери таблу',
selectList: 'Изабери списак',
selectPermissions_title: 'Изабери одобрења',
selectProject: 'Изабери пројекат',
settings: 'Подешавања',
sortList_title: 'Сложи списак',
stopwatch: 'Штоперица',
subscribeToMyOwnCardsByDefault: 'Подразумевано се претплати на сопствене картице',
taskActions_title: 'Радње над задатком',
tasks: 'Задаци',
thereIsNoPreviewAvailableForThisAttachment: 'Нема прегледа доступног за овај прилог.',
time: 'Време',
title: 'Наслов',
userActions_title: 'Корисничке радње',
userAddedThisCardToList: '<0>{{user}}</0><1> је додао ову картицу на {{list}}</1>',
userLeftNewCommentToCard: '{{user}} је оставио нови коментар «{{comment}}» у <2>{{card}}</2>',
userMovedCardFromListToList:
'{{user}} је преместио <2>{{card}}</2> са {{fromList}} у {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}</0><1> је преместио ову картицу са {{fromList}} на {{toList}}</1>',
username: 'Корисничко име',
usernameAlreadyInUse: 'Корисничко име је већ у употреби',
users: 'Корисници',
version: 'Верзија',
viewer: 'Прегледач',
writeComment: 'Напиши коментар...',
},
action: {
addAnotherCard: 'Додај још једну картицу',
addAnotherList: 'Додај још један списак',
addAnotherTask: 'Додај још један задатак',
addCard: 'Додај картицу',
addCard_title: 'Додај картицу',
addComment: 'Додај коментар',
addList: 'Додај списак',
addMember: 'Додај члана',
addMoreDetailedDescription: 'Додај детаљнији опис',
addTask: 'Додај задатак',
addToCard: 'Додај на картицу',
addUser: 'Додај корисника',
copyLink_title: 'Копирај везу',
createBoard: 'Направи таблу',
createFile: 'Направи датотеку',
createLabel: 'Направи ознаку',
createNewLabel: 'Направи нову ознаку',
createProject: 'Направи пројекат',
delete: 'Обриши',
deleteAttachment: 'Обриши прилог',
deleteAvatar: 'Обриши аватара',
deleteBoard: 'Обриши таблу',
deleteCard: 'Обриши картицу',
deleteCard_title: 'Обриши картицу',
deleteComment: 'Обриши коментар',
deleteImage: 'Обриши слику',
deleteLabel: 'Обриши ознаку',
deleteList: 'Обриши списак',
deleteList_title: 'Обриши списак',
deleteProject: 'Обриши пројекат',
deleteProject_title: 'Обриши пројекат',
deleteTask: 'Обриши задатак',
deleteTask_title: 'Обриши задатак',
deleteUser: 'Обриши корисника',
duplicate: 'Клонирај',
duplicateCard_title: 'Клонирај картицу',
edit: 'Измени',
editDueDate_title: 'Измени рок',
editDescription_title: 'Измени опис',
editEmail_title: 'Измени е-пошту',
editInformation_title: 'Измени информације',
editPassword_title: 'Измени лозинку',
editPermissions: 'Измени одобрења',
editStopwatch_title: 'Измени штоперицу',
editTitle_title: 'Измени наслов',
editUsername_title: 'Измени корисничко име',
hideDetails: 'Сакриј детаље',
import: 'Увези',
leaveBoard: 'Напусти таблу',
leaveProject: 'Напусти пројекат',
logOut_title: 'Одјава',
makeCover_title: 'Направи омот',
move: 'Премести',
moveCard_title: 'Премести картицу',
remove: 'Уклони',
removeBackground: 'Уклони позадину',
removeCover_title: 'Уклони омот',
removeFromBoard: 'Уклони са табле',
removeFromProject: 'Уклони из пројекта',
removeManager: 'Уклони руководиоца',
removeMember: 'Уклони члана',
save: 'Сачувај',
showAllAttachments: 'Прикажи све ({{hidden}} сакривене прилоге)',
showDetails: 'Прикажи детаље',
showFewerAttachments: 'Прикажи мање прилога',
sortList_title: 'Сложи списак',
start: 'Почни',
stop: 'Заустави',
subscribe: 'Претплати се',
unsubscribe: 'Укини претплату',
uploadNewAvatar: 'Постави нови аватар',
uploadNewImage: 'Постави нову слику',
},
},
};

View file

@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'sr-Cyrl-CS',
country: 'rs',
name: 'Српски (ћирилица)',
embeddedLocale: login,
};

View file

@ -0,0 +1,23 @@
export default {
translation: {
common: {
emailOrUsername: 'Е-пошта или корисничко име',
invalidEmailOrUsername: 'Неисправна е-пошта или корисничко име',
invalidCredentials: 'Неисправни акредитиви',
invalidPassword: 'Неисправна лозинка',
logInToPlanka: 'Пријавите се у Planka',
noInternetConnection: 'Нема конекције са интернетом',
pageNotFound_title: 'Страница није пронађена',
password: 'Лозинка',
projectManagement: 'Управљање пројектима',
serverConnectionFailed: 'Неуспешна конекција са сервером',
unknownError: 'Непозната грешка, покушајте поново касније',
useSingleSignOn: 'Користи универзалну пријаву',
},
action: {
logIn: 'Пријава',
logInWithSSO: 'Пријава са УП',
},
},
};

View file

@ -0,0 +1,253 @@
import dateFns from 'date-fns/locale/sr-Latn';
export default {
dateFns,
format: {
date: 'd.M.yyyy.',
time: 'p',
dateTime: '$t(format:date) $t(format:time)',
longDate: 'd. MMM',
longDateTime: "d. MMMM 'u' p",
fullDate: 'd. MMM y',
fullDateTime: "d. MMMM y 'u' p",
},
translation: {
common: {
aboutPlanka: 'O Planka',
account: 'Nalog',
actions: 'Radnje',
addAttachment_title: 'Dodaj prilog',
addComment: 'Dodaj komentar',
addManager_title: 'Dodaj rukovodioca',
addMember_title: 'Dodaj člana',
addUser_title: 'Dodaj korisnika',
administrator: 'Administrator',
all: 'Sve',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Sve promene će automatski biti sačuvane<br />nakon uspostavljanja konekcije.',
areYouSureYouWantToDeleteThisAttachment: 'Da li zaista želite da obrišete ovaj prilog?',
areYouSureYouWantToDeleteThisBoard: 'Da li zaista želite da obrišete ovu tablu?',
areYouSureYouWantToDeleteThisCard: 'Da li zaista želite da obrišete ovu karticu?',
areYouSureYouWantToDeleteThisComment: 'Da li zaista želite da obrišete ovaj komentar?',
areYouSureYouWantToDeleteThisLabel: 'Da li zaista želite da obrišete ovu oznaku?',
areYouSureYouWantToDeleteThisList: 'Da li zaista želite da obrišete ovaj spisak?',
areYouSureYouWantToDeleteThisProject: 'Da li zaista želite da obrišete ovaj projekat?',
areYouSureYouWantToDeleteThisTask: 'Da li zaista želite da obrišete ovaj zadatak?',
areYouSureYouWantToDeleteThisUser: 'Da li zaista želite da obrišete ovog korisnika?',
areYouSureYouWantToLeaveBoard: 'Da li zaista želite da napustite ovu tablu?',
areYouSureYouWantToLeaveProject: 'Da li zaista želite da napustite ovaj projekat?',
areYouSureYouWantToRemoveThisManagerFromProject:
'Da li zaista želite da uklonite ovog rukovodioca iz ovog projekta?',
areYouSureYouWantToRemoveThisMemberFromBoard:
'Da li zaista želite da uklonite ovog člana iz ove table?',
attachment: 'Prilog',
attachments: 'Prilozi',
authentication: 'Autentifikacija',
background: 'Pozadina',
board: 'Tabla',
boardNotFound_title: 'Tabla nije pronađena',
canComment: 'Može da komentariše',
canEditContentOfBoard: 'Može da uređuje sadržaj table.',
canOnlyViewBoard: 'Može samo da pregleda tablu.',
cardActions_title: 'Radnje nad karticom',
cardNotFound_title: 'Kartica nije pronađena',
cardOrActionAreDeleted: 'Kartica ili radnja su obrisane.',
color: 'Boja',
copy_inline: 'kopija',
createBoard_title: 'Napravi tablu',
createLabel_title: 'Napravi oznaku',
createNewOneOrSelectExistingOne: 'Napravi novu ili izaberi<br />postojeću.',
createProject_title: 'Napravi projekat',
createTextFile_title: 'Napravi tekstualnu datoteku',
currentPassword: 'Trenutna lozinka',
dangerZone_title: 'Opasna zona',
date: 'Datum',
dueDate: 'Rok',
dueDate_title: 'Rok',
deleteAttachment_title: 'Obriši prilog',
deleteBoard_title: 'Obriši tablu',
deleteCard_title: 'Obriši karticu',
deleteComment_title: 'Obriši komentar',
deleteLabel_title: 'Obriši oznaku',
deleteList_title: 'Obriši spisak',
deleteProject_title: 'Obriši projekat',
deleteTask_title: 'Obriši zadatak',
deleteUser_title: 'Obriši korisnika',
description: 'Opis',
detectAutomatically: 'Detektuj automatski',
dropFileToUpload: 'Prevuci datoteku za slanje',
editor: 'Uređivač',
editAttachment_title: 'Uredi prilog',
editAvatar_title: 'Uredi avatara',
editBoard_title: 'Uredi tablu',
editDueDate_title: 'Uredi rok',
editEmail_title: 'Uredi e-poštu',
editInformation_title: 'Uredi informacije',
editLabel_title: 'Uredi oznaku',
editPassword_title: 'Izmeni lozinku',
editPermissions_title: 'Uredi ovlašćenja',
editStopwatch_title: 'Uredi štopericu',
editUsername_title: 'Izmeni korisničko ime',
email: 'E-pošta',
emailAlreadyInUse: 'E-pošta je već u upotrebi',
enterCardTitle: 'Unesi naslov kartice... [Ctrl+Enter] da se automatski otvori.',
enterDescription: 'Unesi opis...',
enterFilename: 'Unesi naziv datoteke',
enterListTitle: 'Unesi naslov spiska...',
enterProjectTitle: 'Unesi naslov projekta',
enterTaskDescription: 'Unesi opis zadatka...',
filterByLabels_title: 'Filtriraj prema oznakama',
filterByMembers_title: 'Filtriraj prema članovima',
fromComputer_title: 'Sa računara',
fromTrello: 'Sa Trello-a',
general: 'Opšte',
hours: 'Sati',
importBoard_title: 'Uvezi tablu',
invalidCurrentPassword: 'Neispravna trenutna lozinka',
labels: 'Oznake',
language: 'Jezik',
leaveBoard_title: 'Napusti tablu',
leaveProject_title: 'Napusti projekat',
linkIsCopied: 'Veza je iskopirana',
list: 'Spisak',
listActions_title: 'Radnje nad spiskom',
managers: 'Rukovodioci',
managerActions_title: 'Radnje nad rukovodiocima',
members: 'Članovi',
memberActions_title: 'Radnje nad članovima',
minutes: 'Minuti',
moveCard_title: 'Premesti karticu',
name: 'Ime',
newestFirst: 'Prvo najnovije',
newEmail: 'Nova e-pošta',
newPassword: 'Nova lozinka',
newUsername: 'Novo korisničko ime',
noConnectionToServer: 'Nema konekcije sa serverom',
noBoards: 'Nema tabli',
noLists: 'Nema spiskova',
noProjects: 'Nema projekata',
notifications: 'Obaveštenja',
noUnreadNotifications: 'Nema nepročitanih obaveštenja.',
oldestFirst: 'Prvo najstarije',
openBoard_title: 'Otvori tablu',
optional_inline: 'opciono',
organization: 'Organizacija',
phone: 'Telefon',
preferences: 'Svojstva',
pressPasteShortcutToAddAttachmentFromClipboard:
'Savet: pritisni Ctrl-V (Cmd-V na Meku) da bi dodao prilog sa beležnice.',
project: 'Projekat',
projectNotFound_title: 'Projekat nije pronađen',
removeManager_title: 'Ukloni rukovodioca',
removeMember_title: 'Ukloni člana',
searchLabels: 'Pretraži oznake...',
searchMembers: 'Pretraži članove...',
searchUsers: 'Pretraži korisnike...',
searchCards: 'Pretraži kartice...',
seconds: 'Sekunde',
selectBoard: 'Izaberi tablu',
selectList: 'Izaberi spisak',
selectPermissions_title: 'Izaberi odobrenja',
selectProject: 'Izaberi projekat',
settings: 'Podešavanja',
sortList_title: 'Složi spisak',
stopwatch: 'Štoperica',
subscribeToMyOwnCardsByDefault: 'Podrazumevano se pretplati na sopstvene kartice',
taskActions_title: 'Radnje nad zadatkom',
tasks: 'Zadaci',
thereIsNoPreviewAvailableForThisAttachment: 'Nema pregleda dostupnog za ovaj prilog.',
time: 'Vreme',
title: 'Naslov',
userActions_title: 'Korisničke radnje',
userAddedThisCardToList: '<0>{{user}}</0><1> je dodao ovu karticu na {{list}}</1>',
userLeftNewCommentToCard: '{{user}} je ostavio novi komentar «{{comment}}» u <2>{{card}}</2>',
userMovedCardFromListToList:
'{{user}} je premestio <2>{{card}}</2> sa {{fromList}} u {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}</0><1> je premestio ovu karticu sa {{fromList}} na {{toList}}</1>',
username: 'Korisničko ime',
usernameAlreadyInUse: 'Korisničko ime je već u upotrebi',
users: 'Korisnici',
version: 'Verzija',
viewer: 'Pregledač',
writeComment: 'Napiši komentar...',
},
action: {
addAnotherCard: 'Dodaj još jednu karticu',
addAnotherList: 'Dodaj još jedan spisak',
addAnotherTask: 'Dodaj još jedan zadatak',
addCard: 'Dodaj karticu',
addCard_title: 'Dodaj karticu',
addComment: 'Dodaj komentar',
addList: 'Dodaj spisak',
addMember: 'Dodaj člana',
addMoreDetailedDescription: 'Dodaj detaljniji opis',
addTask: 'Dodaj zadatak',
addToCard: 'Dodaj na karticu',
addUser: 'Dodaj korisnika',
copyLink_title: 'Kopiraj vezu',
createBoard: 'Napravi tablu',
createFile: 'Napravi datoteku',
createLabel: 'Napravi oznaku',
createNewLabel: 'Napravi novu oznaku',
createProject: 'Napravi projekat',
delete: 'Obriši',
deleteAttachment: 'Obriši prilog',
deleteAvatar: 'Obriši avatara',
deleteBoard: 'Obriši tablu',
deleteCard: 'Obriši karticu',
deleteCard_title: 'Obriši karticu',
deleteComment: 'Obriši komentar',
deleteImage: 'Obriši sliku',
deleteLabel: 'Obriši oznaku',
deleteList: 'Obriši spisak',
deleteList_title: 'Obriši spisak',
deleteProject: 'Obriši projekat',
deleteProject_title: 'Obriši projekat',
deleteTask: 'Obriši zadatak',
deleteTask_title: 'Obriši zadatak',
deleteUser: 'Obriši korisnika',
duplicate: 'Kloniraj',
duplicateCard_title: 'Kloniraj karticu',
edit: 'Izmeni',
editDueDate_title: 'Izmeni rok',
editDescription_title: 'Izmeni opis',
editEmail_title: 'Izmeni e-poštu',
editInformation_title: 'Izmeni informacije',
editPassword_title: 'Izmeni lozinku',
editPermissions: 'Izmeni odobrenja',
editStopwatch_title: 'Izmeni štopericu',
editTitle_title: 'Izmeni naslov',
editUsername_title: 'Izmeni korisničko ime',
hideDetails: 'Sakrij detalje',
import: 'Uvezi',
leaveBoard: 'Napusti tablu',
leaveProject: 'Napusti projekat',
logOut_title: 'Odjava',
makeCover_title: 'Napravi omot',
move: 'Premesti',
moveCard_title: 'Premesti karticu',
remove: 'Ukloni',
removeBackground: 'Ukloni pozadinu',
removeCover_title: 'Ukloni omot',
removeFromBoard: 'Ukloni sa table',
removeFromProject: 'Ukloni iz projekta',
removeManager: 'Ukloni rukovodioca',
removeMember: 'Ukloni člana',
save: 'Sačuvaj',
showAllAttachments: 'Prikaži sve ({{hidden}} sakrivene priloge)',
showDetails: 'Prikaži detalje',
showFewerAttachments: 'Prikaži manje priloga',
sortList_title: 'Složi spisak',
start: 'Počni',
stop: 'Zaustavi',
subscribe: 'Pretplati se',
unsubscribe: 'Ukini pretplatu',
uploadNewAvatar: 'Postavi novi avatar',
uploadNewImage: 'Postavi novu sliku',
},
},
};

View file

@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'sr-Latn-CS',
country: 'rs',
name: 'Srpski (latinica)',
embeddedLocale: login,
};

View file

@ -0,0 +1,23 @@
export default {
translation: {
common: {
emailOrUsername: 'E-pošta ili korisničko ime',
invalidEmailOrUsername: 'Neispravna e-pošta ili korisničko ime',
invalidCredentials: 'Neispravni akreditivi',
invalidPassword: 'Neispravna lozinka',
logInToPlanka: 'Prijavite se u Planka',
noInternetConnection: 'Nema konekcije sa internetom',
pageNotFound_title: 'Stranica nije pronađena',
password: 'Lozinka',
projectManagement: 'Upravljanje projektima',
serverConnectionFailed: 'Neuspešna konekcija sa serverom',
unknownError: 'Nepoznata greška, pokušajte ponovo kasnije',
useSingleSignOn: 'Koristi univerzalnu prijavu',
},
action: {
logIn: 'Prijava',
logInWithSSO: 'Prijava sa UP',
},
},
};

View file

@ -142,6 +142,15 @@
}
:global(#app) {
&.dragging>* {
pointer-events: none;
}
&.dragScrolling>* {
pointer-events: none;
user-select: none;
}
/* Backgrounds */
.backgroundBerryRed {

View file

@ -1 +1 @@
export default '1.24.0';
export default '1.24.3';

View file

@ -1,4 +1,4 @@
FROM node:18-alpine as server-dependencies
FROM node:18-alpine AS server-dependencies
RUN apk -U upgrade \
&& apk add build-base python3 \
@ -6,7 +6,7 @@ RUN apk -U upgrade \
WORKDIR /app/client
COPY package.json package-lock.json /app/client/
RUN npm install npm@latest --global \
RUN npm install npm --global \
&& npm install pnpm --global \
&& pnpm import \
&& pnpm install

View file

@ -1,4 +1,4 @@
FROM node:18-alpine as server-dependencies
FROM node:18-alpine AS server-dependencies
RUN apk -U upgrade \
&& apk add build-base python3 \
@ -8,7 +8,7 @@ WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install npm@latest --global \
RUN npm install npm --global \
&& npm install pnpm --global \
&& pnpm import \
&& pnpm install

View file

@ -9,36 +9,36 @@ PLANKA_DOCKER_CONTAINER_PLANKA="planka_planka_1"
# Create Temporary folder
BACKUP_DATETIME=$(date --utc +%FT%H-%M-%SZ)
mkdir -p $BACKUP_DATETIME-backup
mkdir -p "$BACKUP_DATETIME-backup"
# Dump DB into SQL File
echo -n "Exporting postgres database ... "
docker exec -t $PLANKA_DOCKER_CONTAINER_POSTGRES pg_dumpall -c -U postgres > $BACKUP_DATETIME-backup/postgres.sql
docker exec -t "$PLANKA_DOCKER_CONTAINER_POSTGRES" pg_dumpall -c -U postgres > "$BACKUP_DATETIME-backup/postgres.sql"
echo "Success!"
# Export Docker Voumes
echo -n "Exporting user-avatars ... "
docker run --rm --volumes-from $PLANKA_DOCKER_CONTAINER_PLANKA -v $(pwd)/$BACKUP_DATETIME-backup:/backup ubuntu cp -r /app/public/user-avatars /backup/user-avatars
docker run --rm --volumes-from "$PLANKA_DOCKER_CONTAINER_PLANKA" -v "$(pwd)/$BACKUP_DATETIME-backup:/backup" ubuntu cp -r /app/public/user-avatars /backup/user-avatars
echo "Success!"
echo -n "Exporting project-background-images ... "
docker run --rm --volumes-from $PLANKA_DOCKER_CONTAINER_PLANKA -v $(pwd)/$BACKUP_DATETIME-backup:/backup ubuntu cp -r /app/public/project-background-images /backup/project-background-images
docker run --rm --volumes-from "$PLANKA_DOCKER_CONTAINER_PLANKA" -v "$(pwd)/$BACKUP_DATETIME-backup:/backup" ubuntu cp -r /app/public/project-background-images /backup/project-background-images
echo "Success!"
echo -n "Exporting attachments ... "
docker run --rm --volumes-from $PLANKA_DOCKER_CONTAINER_PLANKA -v $(pwd)/$BACKUP_DATETIME-backup:/backup ubuntu cp -r /app/private/attachments /backup/attachments
docker run --rm --volumes-from "$PLANKA_DOCKER_CONTAINER_PLANKA" -v "$(pwd)/$BACKUP_DATETIME-backup:/backup" ubuntu cp -r /app/private/attachments /backup/attachments
echo "Success!"
# Create tgz
echo -n "Creating final tarball $BACKUP_DATETIME-backup.tgz ... "
tar -czf $BACKUP_DATETIME-backup.tgz \
$BACKUP_DATETIME-backup/postgres.sql \
$BACKUP_DATETIME-backup/user-avatars \
$BACKUP_DATETIME-backup/project-background-images \
$BACKUP_DATETIME-backup/attachments
tar -czf "$BACKUP_DATETIME-backup.tgz" \
"$BACKUP_DATETIME-backup/postgres.sql" \
"$BACKUP_DATETIME-backup/user-avatars" \
"$BACKUP_DATETIME-backup/project-background-images" \
"$BACKUP_DATETIME-backup/attachments"
echo "Success!"
#Remove source files
echo -n "Cleaning up temporary files and folders ... "
rm -rf $BACKUP_DATETIME-backup
rm -rf "$BACKUP_DATETIME-backup"
echo "Success!"
echo "Backup Complete!"

View file

@ -9,29 +9,29 @@ PLANKA_DOCKER_CONTAINER_PLANKA="planka_planka_1"
# Extract tgz archive
PLANKA_BACKUP_ARCHIVE_TGZ=$1
PLANKA_BACKUP_ARCHIVE=$(basename $PLANKA_BACKUP_ARCHIVE_TGZ .tgz)
PLANKA_BACKUP_ARCHIVE=$(basename "$PLANKA_BACKUP_ARCHIVE_TGZ" .tgz)
echo -n "Extracting tarball $PLANKA_BACKUP_ARCHIVE_TGZ ... "
tar -xzf $PLANKA_BACKUP_ARCHIVE_TGZ
tar -xzf "$PLANKA_BACKUP_ARCHIVE_TGZ"
echo "Success!"
# Import Database
echo -n "Importing postgres database ... "
cat $PLANKA_BACKUP_ARCHIVE/postgres.sql | docker exec -i $PLANKA_DOCKER_CONTAINER_POSTGRES psql -U postgres
cat "$PLANKA_BACKUP_ARCHIVE/postgres.sql" | docker exec -i "$PLANKA_DOCKER_CONTAINER_POSTGRES" psql -U postgres
echo "Success!"
# Restore Docker Volumes
echo -n "Importing user-avatars ... "
docker run --rm --volumes-from $PLANKA_DOCKER_CONTAINER_PLANKA -v $(pwd)/$PLANKA_BACKUP_ARCHIVE:/backup ubuntu cp -rf /backup/user-avatars /app/public/
docker run --rm --volumes-from "$PLANKA_DOCKER_CONTAINER_PLANKA" -v "$(pwd)/$PLANKA_BACKUP_ARCHIVE:/backup" ubuntu cp -rf /backup/user-avatars /app/public/
echo "Success!"
echo -n "Importing project-background-images ... "
docker run --rm --volumes-from $PLANKA_DOCKER_CONTAINER_PLANKA -v $(pwd)/$PLANKA_BACKUP_ARCHIVE:/backup ubuntu cp -rf /backup/project-background-images /app/public/
docker run --rm --volumes-from "$PLANKA_DOCKER_CONTAINER_PLANKA" -v "$(pwd)/$PLANKA_BACKUP_ARCHIVE:/backup" ubuntu cp -rf /backup/project-background-images /app/public/
echo "Success!"
echo -n "Importing attachments ... "
docker run --rm --volumes-from $PLANKA_DOCKER_CONTAINER_PLANKA -v $(pwd)/$PLANKA_BACKUP_ARCHIVE:/backup ubuntu cp -rf /backup/attachments /app/private/
docker run --rm --volumes-from "$PLANKA_DOCKER_CONTAINER_PLANKA" -v "$(pwd)/$PLANKA_BACKUP_ARCHIVE:/backup" ubuntu cp -rf /backup/attachments /app/private/
echo "Success!"
echo -n "Cleaning up temporary files and folders ... "
rm -r $PLANKA_BACKUP_ARCHIVE
rm -r "$PLANKA_BACKUP_ARCHIVE"
echo "Success!"
echo "Restore complete!"

104
package-lock.json generated
View file

@ -1,31 +1,31 @@
{
"name": "planka",
"version": "1.24.0",
"version": "1.24.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "planka",
"version": "1.24.0",
"version": "1.24.3",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
"concurrently": "^8.2.2",
"genversion": "^3.2.0",
"husky": "^9.1.6",
"husky": "^9.1.7",
"lint-staged": "^15.2.10"
},
"devDependencies": {
"eslint": "^8.57.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"prettier": "^3.3.3"
}
},
"node_modules/@babel/runtime": {
"version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz",
"integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==",
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@ -34,24 +34,27 @@
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
"integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
"integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==",
"dev": true,
"dependencies": {
"eslint-visitor-keys": "^3.3.0"
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
},
"peerDependencies": {
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
}
},
"node_modules/@eslint-community/regexpp": {
"version": "4.11.1",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz",
"integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==",
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
"integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
"dev": true,
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
@ -81,22 +84,22 @@
}
},
"node_modules/@eslint/js": {
"version": "8.57.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
"integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
"integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
"integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
"integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
"deprecated": "Use @eslint/config-array instead",
"dev": true,
"dependencies": {
"@humanwhocodes/object-schema": "^2.0.2",
"@humanwhocodes/object-schema": "^2.0.3",
"debug": "^4.3.1",
"minimatch": "^3.0.5"
},
@ -178,9 +181,9 @@
"dev": true
},
"node_modules/acorn": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
@ -466,9 +469,9 @@
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@ -578,16 +581,17 @@
}
},
"node_modules/eslint": {
"version": "8.57.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
"integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.4",
"@eslint/js": "8.57.0",
"@humanwhocodes/config-array": "^0.11.14",
"@eslint/js": "8.57.1",
"@humanwhocodes/config-array": "^0.13.0",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
"@ungap/structured-clone": "^1.2.0",
@ -910,9 +914,9 @@
}
},
"node_modules/flatted": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz",
"integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
"dev": true
},
"node_modules/fs.realpath": {
@ -946,9 +950,9 @@
}
},
"node_modules/get-east-asian-width": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz",
"integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
"integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==",
"engines": {
"node": ">=18"
},
@ -1038,9 +1042,9 @@
}
},
"node_modules/husky": {
"version": "9.1.6",
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.6.tgz",
"integrity": "sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==",
"version": "9.1.7",
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
"integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
"bin": {
"husky": "bin.js"
},
@ -1293,9 +1297,9 @@
}
},
"node_modules/listr2": {
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz",
"integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==",
"version": "8.2.5",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz",
"integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==",
"dependencies": {
"cli-truncate": "^4.0.0",
"colorette": "^2.0.20",
@ -1994,9 +1998,9 @@
}
},
"node_modules/synckit": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz",
"integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==",
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz",
"integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==",
"dev": true,
"dependencies": {
"@pkgr/core": "^0.1.0",
@ -2035,9 +2039,9 @@
}
},
"node_modules/tslib": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/type-check": {
"version": "0.4.0",

View file

@ -1,6 +1,6 @@
{
"name": "planka",
"version": "1.24.0",
"version": "1.24.3",
"private": true,
"homepage": "https://plankanban.github.io/planka",
"repository": {
@ -59,11 +59,11 @@
"dependencies": {
"concurrently": "^8.2.2",
"genversion": "^3.2.0",
"husky": "^9.1.6",
"husky": "^9.1.7",
"lint-staged": "^15.2.10"
},
"devDependencies": {
"eslint": "^8.57.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"prettier": "^3.3.3"

View file

@ -3,6 +3,9 @@ const { v4: uuid } = require('uuid');
const { getRemoteAddress } = require('../../../utils/remoteAddress');
const Errors = {
INVALID_OIDC_CONFIGURATION: {
invalidOIDCConfiguration: 'Invalid OIDC configuration',
},
INVALID_CODE_OR_NONCE: {
invalidCodeOrNonce: 'Invalid code or nonce',
},
@ -37,6 +40,9 @@ module.exports = {
},
exits: {
invalidOIDCConfiguration: {
responseType: 'serverError',
},
invalidCodeOrNonce: {
responseType: 'unauthorized',
},
@ -63,6 +69,7 @@ module.exports = {
sails.log.warn(`Invalid code or nonce! (IP: ${remoteAddress})`);
return Errors.INVALID_CODE_OR_NONCE;
})
.intercept('invalidOIDCConfiguration', () => Errors.INVALID_OIDC_CONFIGURATION)
.intercept('invalidUserinfoConfiguration', () => Errors.INVALID_USERINFO_CONFIGURATION)
.intercept('emailAlreadyInUse', () => Errors.EMAIL_ALREADY_IN_USE)
.intercept('usernameAlreadyInUse', () => Errors.USERNAME_ALREADY_IN_USE)

View file

@ -1,8 +1,26 @@
const Errors = {
INVALID_OIDC_CONFIGURATION: {
invalidOidcConfiguration: 'Invalid OIDC configuration',
},
};
module.exports = {
fn() {
exits: {
invalidOidcConfiguration: {
responseType: 'serverError',
},
},
async fn() {
let oidc = null;
if (sails.hooks.oidc.isActive()) {
const oidcClient = sails.hooks.oidc.getClient();
let oidcClient;
try {
oidcClient = await sails.hooks.oidc.getClient();
} catch (error) {
sails.log.warn(`Error while initializing OIDC client: ${error}`);
throw Errors.INVALID_OIDC_CONFIGURATION;
}
const authorizationUrlParams = {
scope: sails.config.custom.oidcScopes,

View file

@ -11,6 +11,7 @@ module.exports = {
},
exits: {
invalidOIDCConfiguration: {},
invalidCodeOrNonce: {},
invalidUserinfoConfiguration: {},
missingValues: {},
@ -19,7 +20,13 @@ module.exports = {
},
async fn(inputs) {
const client = sails.hooks.oidc.getClient();
let client;
try {
client = await sails.hooks.oidc.getClient();
} catch (error) {
sails.log.warn(`Error while initializing OIDC client: ${error}`);
throw 'invalidOIDCConfiguration';
}
let tokenSet;
try {

View file

@ -15,37 +15,40 @@ module.exports = function defineOidcHook(sails) {
/**
* Runs when this Sails app loads/lifts.
*/
async initialize() {
if (!sails.config.custom.oidcIssuer) {
if (!this.isActive()) {
return;
}
sails.log.info('Initializing custom hook (`oidc`)');
const issuer = await openidClient.Issuer.discover(sails.config.custom.oidcIssuer);
const metadata = {
client_id: sails.config.custom.oidcClientId,
client_secret: sails.config.custom.oidcClientSecret,
redirect_uris: [sails.config.custom.oidcRedirectUri],
response_types: ['code'],
userinfo_signed_response_alg: sails.config.custom.oidcUserinfoSignedResponseAlg,
};
if (sails.config.custom.oidcIdTokenSignedResponseAlg) {
metadata.id_token_signed_response_alg = sails.config.custom.oidcIdTokenSignedResponseAlg;
}
client = new issuer.Client(metadata);
},
getClient() {
async getClient() {
if (client === null && this.isActive()) {
sails.log.info('Initializing OIDC client');
const issuer = await openidClient.Issuer.discover(sails.config.custom.oidcIssuer);
const metadata = {
client_id: sails.config.custom.oidcClientId,
client_secret: sails.config.custom.oidcClientSecret,
redirect_uris: [sails.config.custom.oidcRedirectUri],
response_types: ['code'],
userinfo_signed_response_alg: sails.config.custom.oidcUserinfoSignedResponseAlg,
};
if (sails.config.custom.oidcIdTokenSignedResponseAlg) {
metadata.id_token_signed_response_alg = sails.config.custom.oidcIdTokenSignedResponseAlg;
}
client = new issuer.Client(metadata);
}
return client;
},
isActive() {
return client !== null;
return sails.config.custom.oidcIssuer !== undefined;
},
};
};

View file

@ -27,6 +27,8 @@ const LANGUAGES = [
'ro-RO',
'ru-RU',
'sk-SK',
'sr-Cyrl-CS',
'sr-Latn-CS',
'sv-SE',
'tr-TR',
'uk-UA',

1808
server/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -27,19 +27,19 @@
}
},
"dependencies": {
"@aws-sdk/client-s3": "^3.688.0",
"@aws-sdk/client-s3": "^3.698.0",
"bcrypt": "^5.1.1",
"dotenv": "^16.4.5",
"dotenv-cli": "^7.4.2",
"dotenv-cli": "^7.4.4",
"fs-extra": "^11.2.0",
"jsonwebtoken": "^9.0.2",
"knex": "^3.1.0",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"nodemailer": "^6.9.15",
"openid-client": "^5.7.0",
"nodemailer": "^6.9.16",
"openid-client": "^5.7.1",
"rimraf": "^5.0.10",
"sails": "^1.5.12",
"sails": "^1.5.13",
"sails-hook-orm": "^4.0.3",
"sails-hook-sockets": "^3.0.1",
"sails-postgresql": "^5.0.1",
@ -48,16 +48,16 @@
"stream-to-array": "^2.3.0",
"uuid": "^9.0.1",
"validator": "^13.12.0",
"winston": "^3.14.2",
"winston": "^3.17.0",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"chai": "^4.5.0",
"eslint": "^8.57.0",
"eslint": "^8.57.1",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.30.0",
"mocha": "^10.7.3",
"nodemon": "^3.1.4"
"eslint-plugin-import": "^2.31.0",
"mocha": "^10.8.2",
"nodemon": "^3.1.7"
},
"engines": {
"node": ">=18"