Compare commits

...

13 Commits

Author SHA1 Message Date
Maksim Eltyshev
2173c3657c chore: Update version 2024-10-09 14:25:19 +02:00
Maksim Eltyshev
a2b81f6d59 feat: Add British English translation 2024-10-09 14:22:22 +02:00
Maksim Eltyshev
1948bd4fbe feat: Ability to upload multiple attachments at once
Closes #908
2024-10-09 12:38:18 +02:00
Maksim Eltyshev
52f8abc1f8 docs: Add information about mobile app 2024-10-03 19:07:59 +02:00
Maksim Eltyshev
dd7574f134 ci: Omit version from prebuild asset label 2024-10-02 15:20:10 +02:00
Maksim Eltyshev
19935e52af chore: Update version 2024-10-02 14:18:54 +02:00
Maksim Eltyshev
89292356db feat: Ability to disable SMTP certificate verification
Closes #744
2024-10-02 14:10:31 +02:00
dependabot[bot]
5f6528fa42 chore(deps): Bump rollup from 2.79.1 to 2.79.2 in /client (#898)
Bumps [rollup](https://github.com/rollup/rollup) from 2.79.1 to 2.79.2.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.79.1...v2.79.2)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-02 12:32:10 +02:00
JoeKer1
2081b44874 feat: Support for service annotations in Helm (#903)
Closes #902
2024-10-02 12:30:55 +02:00
Maksim Eltyshev
0b729bf4b3 chore: Update version 2024-09-20 20:41:54 +02:00
Matthew Stickney
368ead982e feat: Configurable file storage locations (#886)
* feat: Make logfile location customizable

It may be desirable to log to a more standard location (e.g. in /var/log/),
or in some cases to turn logging to file off. To support these, use a
custom config property to determine the location of the output log file,
and default to the previous location if it is unset.

* feat: Support alternate storage locations for uploaded files

This involves a couple primary changes:
1) to make Sails' temporary file-upload directory a configurable location
   by using a common file-upload-receiving helper;
2) to create custom static routes for the file-upload locations, so they
   can be outside the application's public directory; and
3) to use the file-uploading handler everywhere that receives files, so
   config for the helper is applied to all file uploads consistently.

This is sufficient to allow the application directory to be deployed read-
only, with writable storage used for file uploads. The new config property
for Sails' temporary upload directory, combined with the existing settings
for user-avatar and background-image locations are sufficient to handle
uploads; the new custom routes handle serving those files from external
locations.

The default behavior of the application should be unchanged, with files
uploaded to, and served from, the public directory if the relevant
config properties aren't set to other values.
2024-09-20 20:29:11 +02:00
iMarKoLiGa
1217969e22 feat: Ability to configure OIDC claims source (#888)
Closes #884
2024-09-20 16:19:54 +02:00
Ahmed
e410e21363 feat: Add Yemeni Arabic translation (#880) 2024-09-18 14:17:51 +02:00
38 changed files with 7745 additions and 7126 deletions

View File

@@ -4,7 +4,6 @@ on:
release:
types: [created]
jobs:
build-and-publish-release-package:
runs-on: ubuntu-latest
@@ -52,6 +51,6 @@ jobs:
- name: Dist upload assets
run: |
gh release upload ${{ github.event.release.tag_name }} planka-prebuild-${{ github.event.release.tag_name }}.zip
gh release upload ${{ github.event.release.tag_name }} planka-prebuild-${{ github.event.release.tag_name }}.zip#planka-prebuild.zip
env:
GH_TOKEN: ${{ github.token }}

View File

@@ -25,6 +25,12 @@ There are many ways to install Planka, [check them out](https://docs.planka.clou
For configuration, please see the [configuration section](https://docs.planka.cloud/docs/category/configuration).
## Mobile app
Here is the [mobile app repository](https://github.com/LouisHDev/planka_app) maintained by the community, where you can build an app for iOS and Android.
Alternatively, you can download the [Android APK](https://github.com/LouisHDev/planka_app/releases/latest/download/app-release.apk) directly.
## Contact
- If you want to get a hosted version of Planka, you can contact us via email contact@planka.cloud

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.8
version: 0.2.11
# 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.22.0"
appVersion: "1.23.2"
dependencies:
- alias: postgresql

View File

@@ -4,6 +4,10 @@ metadata:
name: {{ include "planka.fullname" . }}
labels:
{{- include "planka.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
ports:

View File

@@ -54,6 +54,7 @@ securityContext: {}
# runAsUser: 1000
service:
annotations: {}
type: ClusterIP
port: 1337
## @param service.containerPort Planka HTTP container port

View File

@@ -19229,9 +19229,9 @@
}
},
"node_modules/rollup": {
"version": "2.79.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
"integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
"version": "2.79.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"bin": {
"rollup": "dist/bin/rollup"
},

View File

@@ -28,7 +28,7 @@ const AttachmentAddStep = React.memo(({ onCreate, onClose }) => {
</Popup.Header>
<Popup.Content>
<Menu secondary vertical className={styles.menu}>
<FilePicker onSelect={handleFileSelect}>
<FilePicker multiple onSelect={handleFileSelect}>
<Menu.Item className={styles.menuItem}>
{t('common.fromComputer', {
context: 'title',

View File

@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
import { closePopup } from '../../../lib/popup';
import { useModal } from '../../../hooks';
import { isActiveTextElement } from '../../../utils/element-helpers';
import TextFileAddModal from './TextFileAddModal';
import styles from './AttachmentAddZone.module.scss';
@@ -24,13 +25,14 @@ const AttachmentAddZone = React.memo(({ children, onCreate }) => {
const handleDropAccepted = useCallback(
(files) => {
submit(files[0]);
files.forEach((file) => {
submit(file);
});
},
[submit],
);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
multiple: false,
noClick: true,
noKeyboard: true,
onDropAccepted: handleDropAccepted,
@@ -49,38 +51,43 @@ const AttachmentAddZone = React.memo(({ children, onCreate }) => {
return;
}
const file = event.clipboardData.files[0];
const { files, items } = event.clipboardData;
if (file) {
submit(file);
return;
}
const item = event.clipboardData.items[0];
if (!item) {
return;
}
if (item.kind === 'file') {
submit(item.getAsFile());
return;
}
if (
['input', 'textarea'].includes(event.target.tagName.toLowerCase()) &&
event.target === document.activeElement
) {
return;
}
closePopup();
event.preventDefault();
item.getAsString((content) => {
openModal({
content,
if (files.length > 0) {
[...files].forEach((file) => {
submit(file);
});
return;
}
if (items.length === 0) {
return;
}
if (items[0].kind === 'string') {
if (isActiveTextElement(event.target)) {
return;
}
closePopup();
event.preventDefault();
items[0].getAsString((content) => {
openModal({
content,
});
});
return;
}
[...items].forEach((item) => {
if (item.kind !== 'file') {
return;
}
submit(item.getAsFile());
});
};

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import styles from './FilePicker.module.css';
const FilePicker = React.memo(({ children, accept, onSelect }) => {
const FilePicker = React.memo(({ children, accept, multiple, onSelect }) => {
const field = useRef(null);
const handleTriggerClick = useCallback(() => {
@@ -12,11 +12,11 @@ const FilePicker = React.memo(({ children, accept, onSelect }) => {
const handleFieldChange = useCallback(
({ target }) => {
if (target.files[0]) {
onSelect(target.files[0]);
[...target.files].forEach((file) => {
onSelect(file);
});
target.value = null; // eslint-disable-line no-param-reassign
}
target.value = null; // eslint-disable-line no-param-reassign
},
[onSelect],
);
@@ -32,6 +32,7 @@ const FilePicker = React.memo(({ children, accept, onSelect }) => {
ref={field}
type="file"
accept={accept}
multiple={multiple}
className={styles.field}
onChange={handleFieldChange}
/>
@@ -42,11 +43,13 @@ const FilePicker = React.memo(({ children, accept, onSelect }) => {
FilePicker.propTypes = {
children: PropTypes.element.isRequired,
accept: PropTypes.string,
multiple: PropTypes.bool,
onSelect: PropTypes.func.isRequired,
};
FilePicker.defaultProps = {
accept: undefined,
multiple: false,
};
export default FilePicker;

View File

@@ -0,0 +1,252 @@
import dateFns from 'date-fns/locale/ar';
export default {
dateFns,
format: {
date: 'M/d/yyyy',
time: 'p',
dateTime: '$t(format:date) $t(format:time)',
longDate: 'MMM d',
longDateTime: "MMMM d 'at' p",
fullDate: 'MMM d, y',
fullDateTime: "MMMM d, y 'at' p",
},
translation: {
common: {
aboutPlanka: 'حول 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 على Mac) لإضافة مرفق من الحافظة.',
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}} hidden)',
showDetails: 'إظهار التفاصيل',
showFewerAttachments: 'عرض مرفقات أقل',
sortList_title: 'فرز القائمة',
start: 'ابدأ',
stop: 'توقف',
subscribe: 'اشترك',
unsubscribe: 'إلغاء الاشتراك',
uploadNewAvatar: 'رفع صورة رمزية جديدة',
uploadNewImage: 'رفع صورة جديدة',
},
},
};

View File

@@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'ar-YE',
country: 'ye',
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: 'تسجيل الدخول باستخدام SSO',
},
},
};

View File

@@ -0,0 +1,253 @@
import dateFns from 'date-fns/locale/en-GB';
export default {
dateFns,
format: {
date: 'P',
time: 'p',
dateTime: '$t(format:date) $t(format:time)',
longDate: 'd MMM',
longDateTime: "d MMMM 'at' p",
fullDate: 'd MMM y',
fullDateTime: "d MMMM y 'at' p",
},
translation: {
common: {
aboutPlanka: 'About Planka',
account: 'Account',
actions: 'Actions',
addAttachment_title: 'Add Attachment',
addComment: 'Add comment',
addManager_title: 'Add Manager',
addMember_title: 'Add Member',
addUser_title: 'Add User',
administrator: 'Administrator',
all: 'All',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'All changes will be automatically saved<br />after connection restored.',
areYouSureYouWantToDeleteThisAttachment: 'Are you sure you want to delete this attachment?',
areYouSureYouWantToDeleteThisBoard: 'Are you sure you want to delete this board?',
areYouSureYouWantToDeleteThisCard: 'Are you sure you want to delete this card?',
areYouSureYouWantToDeleteThisComment: 'Are you sure you want to delete this comment?',
areYouSureYouWantToDeleteThisLabel: 'Are you sure you want to delete this label?',
areYouSureYouWantToDeleteThisList: 'Are you sure you want to delete this list?',
areYouSureYouWantToDeleteThisProject: 'Are you sure you want to delete this project?',
areYouSureYouWantToDeleteThisTask: 'Are you sure you want to delete this task?',
areYouSureYouWantToDeleteThisUser: 'Are you sure you want to delete this user?',
areYouSureYouWantToLeaveBoard: 'Are you sure you want to leave the board?',
areYouSureYouWantToLeaveProject: 'Are you sure you want to leave the project?',
areYouSureYouWantToRemoveThisManagerFromProject:
'Are you sure you want to remove this manager from the project?',
areYouSureYouWantToRemoveThisMemberFromBoard:
'Are you sure you want to remove this member from the board?',
attachment: 'Attachment',
attachments: 'Attachments',
authentication: 'Authentication',
background: 'Background',
board: 'Board',
boardNotFound_title: 'Board Not Found',
canComment: 'Can comment',
canEditContentOfBoard: 'Can edit the content of the board.',
canOnlyViewBoard: 'Can only view the board.',
cardActions_title: 'Card Actions',
cardNotFound_title: 'Card Not Found',
cardOrActionAreDeleted: 'Card or action are deleted.',
color: 'Color',
copy_inline: 'copy',
createBoard_title: 'Create Board',
createLabel_title: 'Create Label',
createNewOneOrSelectExistingOne: 'Create a new one or select<br />an existing one.',
createProject_title: 'Create Project',
createTextFile_title: 'Create Text File',
currentPassword: 'Current password',
dangerZone_title: 'Danger Zone',
date: 'Date',
dueDate: 'Due date',
dueDate_title: 'Due Date',
deleteAttachment_title: 'Delete Attachment',
deleteBoard_title: 'Delete Board',
deleteCard_title: 'Delete Card',
deleteComment_title: 'Delete Comment',
deleteLabel_title: 'Delete Label',
deleteList_title: 'Delete List',
deleteProject_title: 'Delete Project',
deleteTask_title: 'Delete Task',
deleteUser_title: 'Delete User',
description: 'Description',
detectAutomatically: 'Detect automatically',
dropFileToUpload: 'Drop file to upload',
editor: 'Editor',
editAttachment_title: 'Edit Attachment',
editAvatar_title: 'Edit Avatar',
editBoard_title: 'Edit Board',
editDueDate_title: 'Edit Due Date',
editEmail_title: 'Edit E-mail',
editInformation_title: 'Edit Information',
editLabel_title: 'Edit Label',
editPassword_title: 'Edit Password',
editPermissions_title: 'Edit Permissions',
editStopwatch_title: 'Edit Stopwatch',
editUsername_title: 'Edit Username',
email: 'E-mail',
emailAlreadyInUse: 'E-mail already in use',
enterCardTitle: 'Enter card title... [Ctrl+Enter] to auto-open.',
enterDescription: 'Enter description...',
enterFilename: 'Enter filename',
enterListTitle: 'Enter list title...',
enterProjectTitle: 'Enter project title',
enterTaskDescription: 'Enter task description...',
filterByLabels_title: 'Filter By Labels',
filterByMembers_title: 'Filter By Members',
fromComputer_title: 'From Computer',
fromTrello: 'From Trello',
general: 'General',
hours: 'Hours',
importBoard_title: 'Import Board',
invalidCurrentPassword: 'Invalid current password',
labels: 'Labels',
language: 'Language',
leaveBoard_title: 'Leave Board',
leaveProject_title: 'Leave Project',
linkIsCopied: 'Link is copied',
list: 'List',
listActions_title: 'List Actions',
managers: 'Managers',
managerActions_title: 'Manager Actions',
members: 'Members',
memberActions_title: 'Member Actions',
minutes: 'Minutes',
moveCard_title: 'Move Card',
name: 'Name',
newestFirst: 'Newest first',
newEmail: 'New e-mail',
newPassword: 'New password',
newUsername: 'New username',
noConnectionToServer: 'No connection to server',
noBoards: 'No boards',
noLists: 'No lists',
noProjects: 'No projects',
notifications: 'Notifications',
noUnreadNotifications: 'No unread notifications.',
oldestFirst: 'Oldest first',
openBoard_title: 'Open Board',
optional_inline: 'optional',
organization: 'Organization',
phone: 'Phone',
preferences: 'Preferences',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: press Ctrl-V (Cmd-V on Mac) to add an attachment from the clipboard.',
project: 'Project',
projectNotFound_title: 'Project Not Found',
removeManager_title: 'Remove Manager',
removeMember_title: 'Remove Member',
searchLabels: 'Search labels...',
searchMembers: 'Search members...',
searchUsers: 'Search users...',
searchCards: 'Search cards...',
seconds: 'Seconds',
selectBoard: 'Select board',
selectList: 'Select list',
selectPermissions_title: 'Select Permissions',
selectProject: 'Select project',
settings: 'Settings',
sortList_title: 'Sort List',
stopwatch: 'Stopwatch',
subscribeToMyOwnCardsByDefault: 'Subscribe to my own cards by default',
taskActions_title: 'Task Actions',
tasks: 'Tasks',
thereIsNoPreviewAvailableForThisAttachment:
'There is no preview available for this attachment.',
time: 'Time',
title: 'Title',
userActions_title: 'User Actions',
userAddedThisCardToList: '<0>{{user}}</0><1> added this card to {{list}}</1>',
userLeftNewCommentToCard: '{{user}} left a new comment «{{comment}}» to <2>{{card}}</2>',
userMovedCardFromListToList: '{{user}} moved <2>{{card}}</2> from {{fromList}} to {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}</0><1> moved this card from {{fromList}} to {{toList}}</1>',
username: 'Username',
usernameAlreadyInUse: 'Username already in use',
users: 'Users',
version: 'Version',
viewer: 'Viewer',
writeComment: 'Write a comment...',
},
action: {
addAnotherCard: 'Add another card',
addAnotherList: 'Add another list',
addAnotherTask: 'Add another task',
addCard: 'Add card',
addCard_title: 'Add Card',
addComment: 'Add comment',
addList: 'Add list',
addMember: 'Add member',
addMoreDetailedDescription: 'Add more detailed description',
addTask: 'Add task',
addToCard: 'Add to card',
addUser: 'Add user',
copyLink_title: 'Copy Link',
createBoard: 'Create board',
createFile: 'Create file',
createLabel: 'Create label',
createNewLabel: 'Create new label',
createProject: 'Create project',
delete: 'Delete',
deleteAttachment: 'Delete attachment',
deleteAvatar: 'Delete avatar',
deleteBoard: 'Delete board',
deleteCard: 'Delete card',
deleteCard_title: 'Delete Card',
deleteComment: 'Delete comment',
deleteImage: 'Delete image',
deleteLabel: 'Delete label',
deleteList: 'Delete list',
deleteList_title: 'Delete List',
deleteProject: 'Delete project',
deleteProject_title: 'Delete Project',
deleteTask: 'Delete task',
deleteTask_title: 'Delete Task',
deleteUser: 'Delete user',
duplicate: 'Duplicate',
duplicateCard_title: 'Duplicate Card',
edit: 'Edit',
editDueDate_title: 'Edit Due Date',
editDescription_title: 'Edit Description',
editEmail_title: 'Edit E-mail',
editInformation_title: 'Edit Information',
editPassword_title: 'Edit Password',
editPermissions: 'Edit permissions',
editStopwatch_title: 'Edit Stopwatch',
editTitle_title: 'Edit Title',
editUsername_title: 'Edit Username',
hideDetails: 'Hide details',
import: 'Import',
leaveBoard: 'Leave board',
leaveProject: 'Leave project',
logOut_title: 'Log Out',
makeCover_title: 'Make Cover',
move: 'Move',
moveCard_title: 'Move Card',
remove: 'Remove',
removeBackground: 'Remove background',
removeCover_title: 'Remove Cover',
removeFromBoard: 'Remove from board',
removeFromProject: 'Remove from project',
removeManager: 'Remove manager',
removeMember: 'Remove member',
save: 'Save',
showAllAttachments: 'Show all attachments ({{hidden}} hidden)',
showDetails: 'Show details',
showFewerAttachments: 'Show fewer attachments',
sortList_title: 'Sort List',
start: 'Start',
stop: 'Stop',
subscribe: 'Subscribe',
unsubscribe: 'Unsubscribe',
uploadNewAvatar: 'Upload new avatar',
uploadNewImage: 'Upload new image',
},
},
};

View File

@@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'en-GB',
country: 'gb',
name: 'English',
embeddedLocale: login,
};

View File

@@ -0,0 +1,23 @@
export default {
translation: {
common: {
emailOrUsername: 'E-mail or username',
invalidEmailOrUsername: 'Invalid e-mail or username',
invalidCredentials: 'Invalid credentials',
invalidPassword: 'Invalid password',
logInToPlanka: 'Log in to Planka',
noInternetConnection: 'No internet connection',
pageNotFound_title: 'Page Not Found',
password: 'Password',
projectManagement: 'Project management',
serverConnectionFailed: 'Server connection failed',
unknownError: 'Unknown error, try again later',
useSingleSignOn: 'Use single sign-on',
},
action: {
logIn: 'Log in',
logInWithSSO: 'Log in with SSO',
},
},
};

View File

@@ -1,7 +1,9 @@
import arYE from './ar-YE';
import bgBG from './bg-BG';
import csCZ from './cs-CZ';
import daDK from './da-DK';
import deDE from './de-DE';
import enGB from './en-GB';
import enUS from './en-US';
import esES from './es-ES';
import faIR from './fa-IR';
@@ -25,10 +27,12 @@ import zhCN from './zh-CN';
import zhTW from './zh-TW';
const locales = [
arYE,
bgBG,
csCZ,
daDK,
deDE,
enGB,
enUS,
esES,
faIR,

View File

@@ -1,5 +1,8 @@
// eslint-disable-next-line import/prefer-default-export
export const focusEnd = (element) => {
element.focus();
element.setSelectionRange(element.value.length + 1, element.value.length + 1);
};
export const isActiveTextElement = (element) =>
['input', 'textarea'].includes(element.tagName.toLowerCase()) &&
element === document.activeElement;

View File

@@ -1 +1 @@
export default '1.22.0';
export default '1.23.2';

View File

@@ -37,6 +37,7 @@ services:
# - OIDC_RESPONSE_MODE=fragment
# - OIDC_USE_DEFAULT_RESPONSE_MODE=true
# - OIDC_ADMIN_ROLES=admin
# - OIDC_CLAIMS_SOURCE=userinfo
# - OIDC_EMAIL_ATTRIBUTE=email
# - OIDC_NAME_ATTRIBUTE=name
# - OIDC_USERNAME_ATTRIBUTE=preferred_username
@@ -53,6 +54,7 @@ services:
# - SMTP_USER=
# - SMTP_PASSWORD=
# - SMTP_FROM="Demo Demo" <demo@demo.demo>
# - SMTP_TLS_REJECT_UNAUTHORIZED=false
# Optional fields: accessToken, events, excludedEvents
# - |

View File

@@ -44,6 +44,7 @@ services:
# - OIDC_RESPONSE_MODE=fragment
# - OIDC_USE_DEFAULT_RESPONSE_MODE=true
# - OIDC_ADMIN_ROLES=admin
# - OIDC_CLAIMS_SOURCE=userinfo
# - OIDC_EMAIL_ATTRIBUTE=email
# - OIDC_NAME_ATTRIBUTE=name
# - OIDC_USERNAME_ATTRIBUTE=preferred_username
@@ -60,6 +61,7 @@ services:
# - SMTP_USER=
# - SMTP_PASSWORD=
# - SMTP_FROM="Demo Demo" <demo@demo.demo>
# - SMTP_TLS_REJECT_UNAUTHORIZED=false
# Optional fields: accessToken, events, excludedEvents
# - |

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "planka",
"version": "1.22.0",
"version": "1.23.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "planka",
"version": "1.22.0",
"version": "1.23.2",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "planka",
"version": "1.22.0",
"version": "1.23.2",
"private": true,
"homepage": "https://plankanban.github.io/planka",
"repository": {

View File

@@ -6,6 +6,8 @@ SECRET_KEY=notsecretkey
## Optional
# LOG_FILE=
# TRUST_PROXY=0
# TOKEN_EXPIRES_IN=365 # In days
@@ -35,6 +37,7 @@ SECRET_KEY=notsecretkey
# OIDC_RESPONSE_MODE=fragment
# OIDC_USE_DEFAULT_RESPONSE_MODE=true
# OIDC_ADMIN_ROLES=admin
# OIDC_CLAIMS_SOURCE=userinfo
# OIDC_EMAIL_ATTRIBUTE=email
# OIDC_NAME_ATTRIBUTE=name
# OIDC_USERNAME_ATTRIBUTE=preferred_username
@@ -50,6 +53,7 @@ SECRET_KEY=notsecretkey
# SMTP_USER=
# SMTP_PASSWORD=
# SMTP_FROM="Demo Demo" <demo@demo.demo>
# SMTP_TLS_REJECT_UNAUTHORIZED=false
# Optional fields: accessToken, events, excludedEvents
# WEBHOOKS='[{

View File

@@ -6,8 +6,8 @@ const Errors = {
INVALID_CODE_OR_NONCE: {
invalidCodeOrNonce: 'Invalid code or nonce',
},
INVALID_USERINFO_SIGNATURE: {
invalidUserinfoSignature: 'Invalid signature on userinfo due to client misconfiguration',
INVALID_USERINFO_CONFIGURATION: {
invalidUserinfoConfiguration: 'Invalid userinfo configuration',
},
EMAIL_ALREADY_IN_USE: {
emailAlreadyInUse: 'Email already in use',
@@ -40,7 +40,7 @@ module.exports = {
invalidCodeOrNonce: {
responseType: 'unauthorized',
},
invalidUserinfoSignature: {
invalidUserinfoConfiguration: {
responseType: 'unauthorized',
},
emailAlreadyInUse: {
@@ -63,7 +63,7 @@ module.exports = {
sails.log.warn(`Invalid code or nonce! (IP: ${remoteAddress})`);
return Errors.INVALID_CODE_OR_NONCE;
})
.intercept('invalidUserinfoSignature', () => Errors.INVALID_USERINFO_SIGNATURE)
.intercept('invalidUserinfoConfiguration', () => Errors.INVALID_USERINFO_CONFIGURATION)
.intercept('emailAlreadyInUse', () => Errors.EMAIL_ALREADY_IN_USE)
.intercept('usernameAlreadyInUse', () => Errors.USERNAME_ALREADY_IN_USE)
.intercept('missingValues', () => Errors.MISSING_VALUES);

View File

@@ -1,6 +1,3 @@
const util = require('util');
const { v4: uuid } = require('uuid');
const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
@@ -61,16 +58,9 @@ module.exports = {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const upload = util.promisify((options, callback) =>
this.req.file('file').upload(options, (error, files) => callback(error, files)),
);
let files;
try {
files = await upload({
saveAs: uuid(),
maxBytes: null,
});
files = await sails.helpers.utils.receiveFile('file', this.req);
} catch (error) {
return exits.uploadError(error.message); // TODO: add error
}

View File

@@ -1,6 +1,3 @@
const util = require('util');
const { v4: uuid } = require('uuid');
const Errors = {
PROJECT_NOT_FOUND: {
projectNotFound: 'Project not found',
@@ -69,16 +66,9 @@ module.exports = {
let boardImport;
if (inputs.importType && Object.values(Board.ImportTypes).includes(inputs.importType)) {
const upload = util.promisify((options, callback) =>
this.req.file('importFile').upload(options, (error, files) => callback(error, files)),
);
let files;
try {
files = await upload({
saveAs: uuid(),
maxBytes: null,
});
files = await sails.helpers.utils.receiveFile('importFile', this.req);
} catch (error) {
return exits.uploadError(error.message); // TODO: add error
}

View File

@@ -1,6 +1,4 @@
const util = require('util');
const rimraf = require('rimraf');
const { v4: uuid } = require('uuid');
const Errors = {
PROJECT_NOT_FOUND: {
@@ -53,16 +51,9 @@ module.exports = {
throw Errors.PROJECT_NOT_FOUND; // Forbidden
}
const upload = util.promisify((options, callback) =>
this.req.file('file').upload(options, (error, files) => callback(error, files)),
);
let files;
try {
files = await upload({
saveAs: uuid(),
maxBytes: null,
});
files = await sails.helpers.utils.receiveFile('file', this.req);
} catch (error) {
return exits.uploadError(error.message); // TODO: add error
}

View File

@@ -1,6 +1,4 @@
const util = require('util');
const rimraf = require('rimraf');
const { v4: uuid } = require('uuid');
const Errors = {
USER_NOT_FOUND: {
@@ -54,16 +52,9 @@ module.exports = {
user = currentUser;
}
const upload = util.promisify((options, callback) =>
this.req.file('file').upload(options, (error, files) => callback(error, files)),
);
let files;
try {
files = await upload({
saveAs: uuid(),
maxBytes: null,
});
files = await sails.helpers.utils.receiveFile('file', this.req);
} catch (error) {
return exits.uploadError(error.message); // TODO: add error
}

View File

@@ -12,7 +12,7 @@ module.exports = {
exits: {
invalidCodeOrNonce: {},
invalidUserinfoSignature: {},
invalidUserinfoConfiguration: {},
missingValues: {},
emailAlreadyInUse: {},
usernameAlreadyInUse: {},
@@ -21,9 +21,9 @@ module.exports = {
async fn(inputs) {
const client = sails.hooks.oidc.getClient();
let userInfo;
let tokenSet;
try {
const tokenSet = await client.callback(
tokenSet = await client.callback(
sails.config.custom.oidcRedirectUri,
{
iss: sails.config.custom.oidcIssuer,
@@ -33,23 +33,36 @@ module.exports = {
nonce: inputs.nonce,
},
);
userInfo = await client.userinfo(tokenSet);
} catch (e) {
if (
e instanceof SyntaxError &&
e.message.includes('Unexpected token e in JSON at position 0')
) {
sails.log.warn('Error while exchanging OIDC code: userinfo response is signed');
throw 'invalidUserinfoSignature';
}
sails.log.warn(`Error while exchanging OIDC code: ${e}`);
} catch (error) {
sails.log.warn(`Error while exchanging OIDC code: ${error}`);
throw 'invalidCodeOrNonce';
}
let claims;
if (sails.config.custom.oidcClaimsSource === 'id_token') {
claims = tokenSet.claims();
} else {
try {
claims = await client.userinfo(tokenSet);
} catch (error) {
let errorText;
if (
error instanceof SyntaxError &&
error.message.includes('Unexpected token e in JSON at position 0')
) {
errorText = 'response is signed';
} else {
errorText = error.toString();
}
sails.log.warn(`Error while fetching OIDC userinfo: ${errorText}`);
throw 'invalidUserinfoConfiguration';
}
}
if (
!userInfo[sails.config.custom.oidcEmailAttribute] ||
!userInfo[sails.config.custom.oidcNameAttribute]
!claims[sails.config.custom.oidcEmailAttribute] ||
!claims[sails.config.custom.oidcNameAttribute]
) {
throw 'missingValues';
}
@@ -58,23 +71,23 @@ module.exports = {
if (sails.config.custom.oidcAdminRoles.includes('*')) {
isAdmin = true;
} else {
const roles = userInfo[sails.config.custom.oidcRolesAttribute];
const roles = claims[sails.config.custom.oidcRolesAttribute];
if (Array.isArray(roles)) {
// Use a Set here to avoid quadratic time complexity
const userRoles = new Set(userInfo[sails.config.custom.oidcRolesAttribute]);
const userRoles = new Set(claims[sails.config.custom.oidcRolesAttribute]);
isAdmin = sails.config.custom.oidcAdminRoles.findIndex((role) => userRoles.has(role)) > -1;
}
}
const values = {
isAdmin,
email: userInfo[sails.config.custom.oidcEmailAttribute],
email: claims[sails.config.custom.oidcEmailAttribute],
isSso: true,
name: userInfo[sails.config.custom.oidcNameAttribute],
name: claims[sails.config.custom.oidcNameAttribute],
subscribeToOwnCards: false,
};
if (!sails.config.custom.oidcIgnoreUsername) {
values.username = userInfo[sails.config.custom.oidcUsernameAttribute];
values.username = claims[sails.config.custom.oidcUsernameAttribute];
}
let user;
@@ -84,7 +97,7 @@ module.exports = {
// concurrently with logging in via OIDC.
let identityProviderUser = await IdentityProviderUser.findOne({
issuer: sails.config.custom.oidcIssuer,
sub: userInfo.sub,
sub: claims.sub,
});
if (identityProviderUser) {
@@ -108,7 +121,7 @@ module.exports = {
identityProviderUser = await IdentityProviderUser.create({
userId: user.id,
issuer: sails.config.custom.oidcIssuer,
sub: userInfo.sub,
sub: claims.sub,
});
}

View File

@@ -0,0 +1,41 @@
const util = require('util');
const { v4: uuid } = require('uuid');
async function doUpload(paramName, req, options) {
const uploadOptions = {
...options,
dirname: options.dirname || sails.config.custom.fileUploadTmpDir,
};
const upload = util.promisify((opts, callback) => {
return req.file(paramName).upload(opts, (error, files) => callback(error, files));
});
return upload(uploadOptions);
}
module.exports = {
friendlyName: 'Receive uploaded file from request',
description:
"Store a file uploaded from a MIME-multipart request part. The request part name must be 'file'; the resulting file will have a unique UUID-based name with the same extension.",
inputs: {
paramName: {
type: 'string',
required: true,
description: 'The MIME multi-part parameter containing the file to receive.',
},
req: {
type: 'ref',
required: true,
description: 'The request to receive the file from.',
},
},
fn: async function modFn(inputs, exits) {
exits.success(
await doUpload(inputs.paramName, inputs.req, {
saveAs: uuid(),
dirname: sails.config.custom.fileUploadTmpDir,
maxBytes: null,
}),
);
},
};

View File

@@ -130,8 +130,8 @@ async function sendWebhook(webhook, event, data, user) {
`Webhook ${webhook.url} failed with status ${response.status} and message: ${message}`,
);
}
} catch (e) {
sails.log.error(`Webhook ${webhook.url} failed with error message: ${e.message}`);
} catch (error) {
sails.log.error(`Webhook ${webhook.url} failed with error: ${error}`);
}
}

View File

@@ -33,6 +33,9 @@ module.exports = function defineSmtpHook(sails) {
user: sails.config.custom.smtpUser,
pass: sails.config.custom.smtpPassword,
},
tls: {
rejectUnauthorized: sails.config.custom.smtpTlsRejectUnauthorized,
},
});
},

View File

@@ -6,10 +6,12 @@
*/
const LANGUAGES = [
'ar-YE',
'bg-BG',
'cs-CZ',
'da-DK',
'de-DE',
'en-GB',
'en-US',
'es-ES',
'fa-IR',

View File

@@ -27,6 +27,9 @@ module.exports.custom = {
tokenExpiresIn: parseInt(process.env.TOKEN_EXPIRES_IN, 10) || 365,
// Location to receive uploaded files in. Default (non-string value) is a Sails-specific location.
fileUploadTmpDir: null,
userAvatarsPath: path.join(sails.config.paths.public, 'user-avatars'),
userAvatarsUrl: `${process.env.BASE_URL}/user-avatars`,
@@ -52,6 +55,7 @@ module.exports.custom = {
oidcResponseMode: process.env.OIDC_RESPONSE_MODE || 'fragment',
oidcUseDefaultResponseMode: process.env.OIDC_USE_DEFAULT_RESPONSE_MODE === 'true',
oidcAdminRoles: process.env.OIDC_ADMIN_ROLES ? process.env.OIDC_ADMIN_ROLES.split(',') : [],
oidcClaimsSource: process.env.OIDC_CLAIMS_SOURCE || 'userinfo',
oidcEmailAttribute: process.env.OIDC_EMAIL_ATTRIBUTE || 'email',
oidcNameAttribute: process.env.OIDC_NAME_ATTRIBUTE || 'name',
oidcUsernameAttribute: process.env.OIDC_USERNAME_ATTRIBUTE || 'preferred_username',
@@ -72,6 +76,7 @@ module.exports.custom = {
smtpUser: process.env.SMTP_USER,
smtpPassword: process.env.SMTP_PASSWORD,
smtpFrom: process.env.SMTP_FROM,
smtpTlsRejectUnauthorized: process.env.SMTP_TLS_REJECT_UNAUTHORIZED !== 'false',
webhooks: JSON.parse(process.env.WEBHOOKS || '[]'), // TODO: validate structure

View File

@@ -1,3 +1,56 @@
const serveStatic = require('serve-static');
const sails = require('sails');
const path = require('path');
// Remove prefix from urlPath, assuming completely matches a subpath of
// urlPath. The result preserves query params and fragment if present
//
// Examples:
// '/foo', '/foo/bar' -> '/bar'
// '/foo', '/foo' -> '/'
// '/foo', '/foo?baz=bux' -> '/?baz=bux'
// '/foo', '/foobar' -> '/foobar'
function removeRoutePrefix(prefix, urlPath) {
if (urlPath.startsWith(prefix)) {
const subpath = urlPath.substring(prefix.length);
if (subpath.startsWith('/')) {
// Prefix matched a complete set of path segments, with a valid path
// remaining.
return subpath;
}
if (subpath.length === 0 || subpath.startsWith('?') || subpath.startsWith('#')) {
// Prefix matched a complete set of path segments, but there is no path
// remaining. Add '/'.
return `/${subpath}`;
}
}
// Either the prefix didn't match at all, or it wasn't a complete path match
// (e.g. we don't want to treat '/foo' as a prefix of '/foobar'). Leave the
// path as-is.
return urlPath;
}
function staticDirServer(prefix, dirFn) {
return function handleReq(req, res, next) {
// Custom config properties are not available when the routes config is
// loaded, so resolve the target value just before serving the request.
const dir = dirFn();
const staticServer = serveStatic(dir, { index: false });
const reqPath = req.url;
if (reqPath.startsWith(prefix)) {
// serve-static treats the request url as a sub-path of
// static root; remove the leading route prefix so the static root
// doesn't have to include the prefix as a subdirectory.
req.url = removeRoutePrefix(prefix, req.url);
return staticServer(req, res, next);
}
return next();
};
}
/**
* Route Mappings
* (sails.config.routes)
@@ -81,6 +134,18 @@ module.exports.routes = {
'GET /api/notifications/:id': 'notifications/show',
'PATCH /api/notifications/:ids': 'notifications/update',
'GET /user-avatars/*': {
fn: staticDirServer('/user-avatars', () => path.resolve(sails.config.custom.userAvatarsPath)),
skipAssets: false,
},
'GET /project-background-images/*': {
fn: staticDirServer('/project-background-images', () =>
path.resolve(sails.config.custom.projectBackgroundImagesPath),
),
skipAssets: false,
},
'GET /attachments/:id/download/:filename': {
action: 'attachments/download',
skipAssets: false,

13932
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -43,6 +43,7 @@
"sails-hook-orm": "^4.0.3",
"sails-hook-sockets": "^3.0.1",
"sails-postgresql": "^5.0.1",
"serve-static": "^1.13.1",
"sharp": "^0.33.5",
"stream-to-array": "^2.3.0",
"uuid": "^9.0.1",

View File

@@ -6,7 +6,8 @@ const winston = require('winston');
*/
const defaultLogTimestampFormat = 'YYYY-MM-DD HH:mm:ss';
const logfile = `${process.cwd()}/logs/planka.log`;
const logfile =
'LOG_FILE' in process.env ? process.env.LOG_FILE : `${process.cwd()}/logs/planka.log`;
/**
* Log level for both console and file log sinks.