Compare commits

..
3 Commits
Author SHA1 Message Date
nephayr 01535ecf51 cover.jpg changed for finalblast CI 2024-09-20 19:14:14 +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
13 changed files with 332 additions and 29 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 559 KiB

After

Width:  |  Height:  |  Size: 184 KiB

+252
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: 'رفع صورة جديدة',
},
},
};
+8
View File
@@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'ar-YE',
country: 'ye',
name: 'العربية',
embeddedLocale: login,
};
+23
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',
},
},
};
+2
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,
+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
+1
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
+1
View File
@@ -35,6 +35,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
@@ -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);
@@ -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,
});
}
+2 -2
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}`);
}
}
+1
View File
@@ -6,6 +6,7 @@
*/
const LANGUAGES = [
'ar-YE',
'bg-BG',
'cs-CZ',
'da-DK',
+1
View File
@@ -52,6 +52,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',