Compare commits

...

8 Commits

Author SHA1 Message Date
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
29 changed files with 7399 additions and 7085 deletions

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.10
# 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.1"
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

@@ -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

@@ -1,3 +1,4 @@
import arYE from './ar-YE';
import bgBG from './bg-BG';
import csCZ from './cs-CZ';
import daDK from './da-DK';
@@ -25,6 +26,7 @@ import zhCN from './zh-CN';
import zhTW from './zh-TW';
const locales = [
arYE,
bgBG,
csCZ,
daDK,

View File

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

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.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "planka",
"version": "1.22.0",
"version": "1.23.1",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "planka",
"version": "1.22.0",
"version": "1.23.1",
"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,6 +6,7 @@
*/
const LANGUAGES = [
'ar-YE',
'bg-BG',
'cs-CZ',
'da-DK',

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.