diff --git a/.github/ISSUE_TEMPLATE/1-bug-report.yml b/.github/ISSUE_TEMPLATE/1-bug-report.yml new file mode 100644 index 00000000..0328ef86 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-bug-report.yml @@ -0,0 +1,53 @@ +name: "🐛 Bug Report" +description: Report a bug found while using Planka +title: "[Bug]: " +labels: ["Type: Bug", "Status: Triage"] +body: + - type: dropdown + id: issue-type + attributes: + label: Where is the problem occurring? + description: Select the part of the application where you encountered the issue. + options: + - "I encountered the problem while using the application (Frontend)" + - "I encountered the problem while interacting with the server (Backend)" + - "I'm not sure" + - type: dropdown + id: browsers + attributes: + label: What browsers are you seeing the problem on? + multiple: true + options: + - Brave + - Chrome + - Firefox + - Microsoft Edge + - Safari + - Other + - type: textarea + id: current-behavior + attributes: + label: Current behaviour + description: A description of what is currently happening, including screenshots and other useful information (**DO NOT INCLUDE PRIVATE INFORMATION**). + placeholder: Currently... + validations: + required: true + - type: textarea + id: desired-behavior + attributes: + label: Desired behaviour + description: A clear description of what you think should happen. + placeholder: In this situation, I expected ... + - type: textarea + id: reproduction + attributes: + label: Steps to reproduce + description: Clearly describe which steps or actions you have taken to arrive at the problem. If you have some experience with the code, please link to the specific pieces of code. + placeholder: I did X, then Y, before arriving at Z, when ERROR ... + validations: + required: true + - type: textarea + id: other + attributes: + label: Other information + description: Any other details? diff --git a/.github/ISSUE_TEMPLATE/2-feature-request.yml b/.github/ISSUE_TEMPLATE/2-feature-request.yml new file mode 100644 index 00000000..25bc11ba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-feature-request.yml @@ -0,0 +1,33 @@ +name: "✨ Feature Request" +description: Suggest a feature or enhancement to improve Planka. +labels: ["Type: Idea"] +body: + - type: dropdown + id: idea-type + attributes: + label: Is this a feature for the backend or frontend? + multiple: true + options: + - Backend + - Frontend + validations: + required: true + - type: textarea + id: feature + attributes: + label: What would you like? + description: A clear description of the feature or enhancement wanted. + placeholder: I'd like to be able to... + validations: + required: true + - type: textarea + id: reason + attributes: + label: Why is this needed? + description: A clear description of why this would be useful to have. + placeholder: I want this because... + - type: textarea + id: other + attributes: + label: Other information + placeholder: Any other details? diff --git a/README.md b/README.md index 5ff3bba2..8ccfca58 100644 --- a/README.md +++ b/README.md @@ -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/docker/pulls/meltyshev/planka) ![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-4M%2B-%23066da5) ![GitHub](https://img.shields.io/github/license/plankanban/planka) ![](https://raw.githubusercontent.com/plankanban/planka/master/demo.gif) diff --git a/charts/planka/Chart.yaml b/charts/planka/Chart.yaml index 5cb0ea74..3c073fec 100644 --- a/charts/planka/Chart.yaml +++ b/charts/planka/Chart.yaml @@ -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.1.33 +version: 0.2.5 # 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.17.5" +appVersion: "1.20.1" dependencies: - alias: postgresql diff --git a/charts/planka/templates/deployment.yaml b/charts/planka/templates/deployment.yaml index d2e179fe..60457a64 100644 --- a/charts/planka/templates/deployment.yaml +++ b/charts/planka/templates/deployment.yaml @@ -90,17 +90,37 @@ spec: value: http://localhost:3000 {{- end }} - name: SECRET_KEY + {{- if .Values.existingSecretkeySecret }} + valueFrom: + secretKeyRef: + name: {{ .Values.existingSecretkeySecret }} + key: key + {{- else }} value: {{ required "A secret key needs to be generated using 'openssl rand -hex 64' and assigned to secretkey." .Values.secretkey }} + {{- end }} - name: TRUST_PROXY value: "0" - name: DEFAULT_ADMIN_EMAIL value: {{ .Values.admin_email }} - - name: DEFAULT_ADMIN_PASSWORD - value: {{ .Values.admin_password }} - name: DEFAULT_ADMIN_NAME value: {{ .Values.admin_name }} + {{- if .Values.existingAdminCredsSecret }} + - name: DEFAULT_ADMIN_USERNAME + valueFrom: + secretKeyRef: + name: {{ .Values.existingAdminCredsSecret }} + key: username + - name: DEFAULT_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.existingAdminCredsSecret }} + key: password + {{- else }} - name: DEFAULT_ADMIN_USERNAME value: {{ .Values.admin_username }} + - name: DEFAULT_ADMIN_PASSWORD + value: {{ .Values.admin_password }} + {{- end }} {{ range $k, $v := .Values.env }} - name: {{ $k | quote }} value: {{ $v | quote }} diff --git a/charts/planka/values.yaml b/charts/planka/values.yaml index a6bc393c..b02c8be8 100644 --- a/charts/planka/values.yaml +++ b/charts/planka/values.yaml @@ -17,6 +17,16 @@ fullnameOverride: "" # Generate a secret using openssl rand -base64 45 secretkey: "" +## @param existingSecretkeySecret Name of an existing secret containing the session key string +## NOTE: Must contain key `key` +## NOTE: When it's set, the secretkey parameter is ignored +existingSecretkeySecret: "" + +## @param existingAdminCredsSecret Name of an existing secret containing the admin username and password +## NOTE: Must contain keys `username` and `password` +## NOTE: When it's set, the `admin_username` and `admin_password` parameters are ignored +existingAdminCredsSecret: "" + # Base url for Planka. Will override `ingress.hosts[0].host` # Defaults to `http://localhost:3000` if ingress is disabled. baseUrl: "" diff --git a/client/.env b/client/.env index 00d6bf89..0da35f5c 100644 --- a/client/.env +++ b/client/.env @@ -1 +1,2 @@ -REACT_APP_VERSION=1.18.1 + +REACT_APP_VERSION=1.20.1 diff --git a/client/package-lock.json b/client/package-lock.json index ae00bd7a..2239bfba 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -12,7 +12,7 @@ "dequal": "^2.0.3", "easymde": "^2.18.0", "history": "^5.3.0", - "i18next": "^23.11.5", + "i18next": "^23.12.2", "i18next-browser-languagedetector": "^8.0.0", "initials": "^3.1.2", "js-cookie": "^3.0.5", @@ -30,7 +30,7 @@ "react-datepicker": "^4.25.0", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", - "react-i18next": "^14.1.2", + "react-i18next": "^15.0.0", "react-input-mask": "^2.0.4", "react-markdown": "^8.0.7", "react-photoswipe-gallery": "^2.2.7", @@ -2007,9 +2007,9 @@ "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" }, "node_modules/@babel/runtime": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.6.tgz", - "integrity": "sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz", + "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -11975,9 +11975,9 @@ } }, "node_modules/i18next": { - "version": "23.11.5", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.5.tgz", - "integrity": "sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==", + "version": "23.12.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.12.2.tgz", + "integrity": "sha512-XIeh5V+bi8SJSWGL3jqbTEBW5oD6rbP5L+E7dVQh1MNTxxYef0x15rhJVcRb7oiuq4jLtgy2SD8eFlf6P2cmqg==", "funding": [ { "type": "individual", @@ -22084,11 +22084,11 @@ "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" }, "node_modules/react-i18next": { - "version": "14.1.2", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.2.tgz", - "integrity": "sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.0.0.tgz", + "integrity": "sha512-2O3IgF4zivg57Q6p6i+ChDgJ371IDcEWbuWC6gvoh5NbkDMs0Q+O7RPr4v61+Se32E0V+LmtwePAeqWZW0bi6g==", "dependencies": { - "@babel/runtime": "^7.23.9", + "@babel/runtime": "^7.24.8", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { diff --git a/client/package.json b/client/package.json index 4d000f74..7e975a5f 100755 --- a/client/package.json +++ b/client/package.json @@ -65,7 +65,7 @@ "dequal": "^2.0.3", "easymde": "^2.18.0", "history": "^5.3.0", - "i18next": "^23.11.5", + "i18next": "^23.12.2", "i18next-browser-languagedetector": "^8.0.0", "initials": "^3.1.2", "js-cookie": "^3.0.5", @@ -83,7 +83,7 @@ "react-datepicker": "^4.25.0", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", - "react-i18next": "^14.1.2", + "react-i18next": "^15.0.0", "react-input-mask": "^2.0.4", "react-markdown": "^8.0.7", "react-photoswipe-gallery": "^2.2.7", diff --git a/client/src/actions/cards.js b/client/src/actions/cards.js index 0b0be6be..47dd2947 100644 --- a/client/src/actions/cards.js +++ b/client/src/actions/cards.js @@ -57,10 +57,15 @@ updateCard.failure = (id, error) => ({ }, }); -const handleCardUpdate = (card) => ({ +const handleCardUpdate = (card, isFetched, cardMemberships, cardLabels, tasks, attachments) => ({ type: ActionTypes.CARD_UPDATE_HANDLE, payload: { card, + isFetched, + cardMemberships, + cardLabels, + tasks, + attachments, }, }); diff --git a/client/src/components/CardModal/CardModal.jsx b/client/src/components/CardModal/CardModal.jsx index ff77aa97..9c9651cc 100755 --- a/client/src/components/CardModal/CardModal.jsx +++ b/client/src/components/CardModal/CardModal.jsx @@ -1,4 +1,4 @@ -import React, { useCallback, useRef } from 'react'; +import React, { useCallback, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; @@ -81,6 +81,7 @@ const CardModal = React.memo( onClose, }) => { const [t] = useTranslation(); + const [isLinkCopied, setIsLinkCopied] = useState(false); const isGalleryOpened = useRef(false); @@ -146,6 +147,14 @@ const CardModal = React.memo( onClose(); }, [onDuplicate, onClose]); + const handleCopyLinkClick = useCallback(() => { + navigator.clipboard.writeText(window.location.href); + setIsLinkCopied(true); + setTimeout(() => { + setIsLinkCopied(false); + }, 5000); + }, []); + const handleGalleryOpen = useCallback(() => { isGalleryOpened.current = true; }, []); @@ -506,6 +515,14 @@ const CardModal = React.memo( {t('action.duplicate')} + { + const { allowAllToCreateProjects } = selectors.selectConfig(state); const { isAdmin } = selectors.selectCurrentUser(state); const projects = selectors.selectProjectsForCurrentUser(state); return { items: projects, - canAdd: isAdmin, + canAdd: allowAllToCreateProjects || isAdmin, }; }; diff --git a/client/src/i18n.js b/client/src/i18n.js index 4f5dda56..26dae90d 100644 --- a/client/src/i18n.js +++ b/client/src/i18n.js @@ -58,9 +58,9 @@ i18n .use(initReactI18next) .init({ resources: embeddedLocales, - fallbackLng: 'en', + fallbackLng: 'en-US', supportedLngs: languages, - load: 'languageOnly', + load: 'currentOnly', interpolation: { escapeValue: false, format(value, format, language) { @@ -80,7 +80,7 @@ i18n }); i18n.loadCoreLocale = async (language = i18n.resolvedLanguage) => { - if (language === 'en') { + if (language === i18n.options.fallbackLng[0]) { return; } diff --git a/client/src/locales/bg/core.js b/client/src/locales/bg-BG/core.js similarity index 100% rename from client/src/locales/bg/core.js rename to client/src/locales/bg-BG/core.js diff --git a/client/src/locales/bg/index.js b/client/src/locales/bg-BG/index.js similarity index 85% rename from client/src/locales/bg/index.js rename to client/src/locales/bg-BG/index.js index 95c609bf..4b1631bf 100644 --- a/client/src/locales/bg/index.js +++ b/client/src/locales/bg-BG/index.js @@ -1,7 +1,7 @@ import login from './login'; export default { - language: 'bg', + language: 'bg-BG', country: 'bg', name: 'Български', embeddedLocale: login, diff --git a/client/src/locales/bg/login.js b/client/src/locales/bg-BG/login.js similarity index 100% rename from client/src/locales/bg/login.js rename to client/src/locales/bg-BG/login.js diff --git a/client/src/locales/cs/core.js b/client/src/locales/cs-CZ/core.js similarity index 100% rename from client/src/locales/cs/core.js rename to client/src/locales/cs-CZ/core.js diff --git a/client/src/locales/cs/index.js b/client/src/locales/cs-CZ/index.js similarity index 84% rename from client/src/locales/cs/index.js rename to client/src/locales/cs-CZ/index.js index b172ade8..a8380e7c 100644 --- a/client/src/locales/cs/index.js +++ b/client/src/locales/cs-CZ/index.js @@ -1,7 +1,7 @@ import login from './login'; export default { - language: 'cs', + language: 'cs-CZ', country: 'cz', name: 'Čeština', embeddedLocale: login, diff --git a/client/src/locales/cs/login.js b/client/src/locales/cs-CZ/login.js similarity index 100% rename from client/src/locales/cs/login.js rename to client/src/locales/cs-CZ/login.js diff --git a/client/src/locales/da/core.js b/client/src/locales/da-DK/core.js similarity index 100% rename from client/src/locales/da/core.js rename to client/src/locales/da-DK/core.js diff --git a/client/src/locales/da/index.js b/client/src/locales/da-DK/index.js similarity index 83% rename from client/src/locales/da/index.js rename to client/src/locales/da-DK/index.js index 9fcfa709..937f5ae4 100644 --- a/client/src/locales/da/index.js +++ b/client/src/locales/da-DK/index.js @@ -1,7 +1,7 @@ import login from './login'; export default { - language: 'da', + language: 'da-DK', country: 'dk', name: 'Dansk', embeddedLocale: login, diff --git a/client/src/locales/da/login.js b/client/src/locales/da-DK/login.js similarity index 100% rename from client/src/locales/da/login.js rename to client/src/locales/da-DK/login.js diff --git a/client/src/locales/de/core.js b/client/src/locales/de-DE/core.js similarity index 98% rename from client/src/locales/de/core.js rename to client/src/locales/de-DE/core.js index 0b743de5..801a50f0 100644 --- a/client/src/locales/de/core.js +++ b/client/src/locales/de-DE/core.js @@ -104,6 +104,7 @@ export default { language: 'Sprache', leaveBoard_title: 'Board verlassen', leaveProject_title: 'Projekt verlassen', + linkIsCopied: 'Link kopiert', list: 'Listen', listActions_title: 'Aufgaben auflisten', managers: 'Manager', @@ -169,6 +170,7 @@ export default { addTask: 'Aufgabe hinzufügen', addToCard: 'Zu Karte hinzufügen', addUser: 'Benutzer hinzufügen', + copyLink_title: 'Link kopieren', createBoard: 'Board erstellen', createFile: 'Datei erstellen', createLabel: 'Label erstellen', @@ -190,6 +192,8 @@ export default { deleteTask: 'Aufgabe löschen', deleteTask_title: 'Aufgabe löschen', deleteUser: 'Benutzer löschen', + duplicate: 'Kopieren', + duplicateCard_title: 'Karte kopieren', edit: 'Bearbeiten', editDueDate_title: 'Fälligkeitsdatum bearbeiten', editDescription_title: 'Beschreibung ändern', diff --git a/client/src/locales/de/index.js b/client/src/locales/de-DE/index.js similarity index 84% rename from client/src/locales/de/index.js rename to client/src/locales/de-DE/index.js index fc655946..2eb9ea7b 100644 --- a/client/src/locales/de/index.js +++ b/client/src/locales/de-DE/index.js @@ -1,7 +1,7 @@ import login from './login'; export default { - language: 'de', + language: 'de-DE', country: 'de', name: 'Deutsch', embeddedLocale: login, diff --git a/client/src/locales/de/login.js b/client/src/locales/de-DE/login.js similarity index 100% rename from client/src/locales/de/login.js rename to client/src/locales/de-DE/login.js diff --git a/client/src/locales/en/core.js b/client/src/locales/en-US/core.js similarity index 99% rename from client/src/locales/en/core.js rename to client/src/locales/en-US/core.js index 2fd51243..75fa8c0c 100644 --- a/client/src/locales/en/core.js +++ b/client/src/locales/en-US/core.js @@ -106,6 +106,7 @@ export default { language: 'Language', leaveBoard_title: 'Leave Board', leaveProject_title: 'Leave Project', + linkIsCopied: 'Link is copied', list: 'List', listActions_title: 'List Actions', managers: 'Managers', @@ -183,6 +184,7 @@ export default { addTask: 'Add task', addToCard: 'Add to card', addUser: 'Add user', + copyLink_title: 'Copy Link', createBoard: 'Create board', createFile: 'Create file', createLabel: 'Create label', diff --git a/client/src/locales/en/index.js b/client/src/locales/en-US/index.js similarity index 81% rename from client/src/locales/en/index.js rename to client/src/locales/en-US/index.js index 73cf0522..3aea6da7 100644 --- a/client/src/locales/en/index.js +++ b/client/src/locales/en-US/index.js @@ -4,8 +4,8 @@ import login from './login'; import core from './core'; export default { - language: 'en', - country: 'gb', + language: 'en-US', + country: 'us', name: 'English', embeddedLocale: merge(login, core), }; diff --git a/client/src/locales/en/login.js b/client/src/locales/en-US/login.js similarity index 100% rename from client/src/locales/en/login.js rename to client/src/locales/en-US/login.js diff --git a/client/src/locales/es/core.js b/client/src/locales/es-ES/core.js similarity index 100% rename from client/src/locales/es/core.js rename to client/src/locales/es-ES/core.js diff --git a/client/src/locales/es/index.js b/client/src/locales/es-ES/index.js similarity index 84% rename from client/src/locales/es/index.js rename to client/src/locales/es-ES/index.js index bda1fe81..d853b3df 100644 --- a/client/src/locales/es/index.js +++ b/client/src/locales/es-ES/index.js @@ -1,7 +1,7 @@ import login from './login'; export default { - language: 'es', + language: 'es-ES', country: 'es', name: 'Español', embeddedLocale: login, diff --git a/client/src/locales/es/login.js b/client/src/locales/es-ES/login.js similarity index 100% rename from client/src/locales/es/login.js rename to client/src/locales/es-ES/login.js diff --git a/client/src/locales/fa-IR/core.js b/client/src/locales/fa-IR/core.js new file mode 100644 index 00000000..358897fc --- /dev/null +++ b/client/src/locales/fa-IR/core.js @@ -0,0 +1,254 @@ +import dateFns from 'date-fns/locale/fa-IR'; + +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: + 'تمام تغییرات به صورت خودکار ذخیره می‌شوند
بعد از بازیابی ارتباط.', + 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: 'یک جدید ایجاد کنید یا
یکی موجود را انتخاب کنید.', + 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}}<1> این کارت را به {{list}} اضافه کرد', + userLeftNewCommentToCard: '{{user}} نظر جدید «{{comment}}» را به <2>{{card}} اضافه کرد', + userMovedCardFromListToList: + '{{user}} <2>{{card}} را از {{fromList}} به {{toList}} منتقل کرد', + userMovedThisCardFromListToList: + '<0>{{user}}<1> این کارت را از {{fromList}} به {{toList}} منتقل کرد', + 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: 'آپلود تصویر جدید', + }, + }, +}; diff --git a/client/src/locales/fa-IR/index.js b/client/src/locales/fa-IR/index.js new file mode 100644 index 00000000..62024311 --- /dev/null +++ b/client/src/locales/fa-IR/index.js @@ -0,0 +1,8 @@ +import login from './login'; + +export default { + language: 'fa-IR', + country: 'ir', + name: 'فارسی', + embeddedLocale: login, +}; diff --git a/client/src/locales/fa-IR/login.js b/client/src/locales/fa-IR/login.js new file mode 100644 index 00000000..776b92dc --- /dev/null +++ b/client/src/locales/fa-IR/login.js @@ -0,0 +1,22 @@ +export default { + translation: { + common: { + emailOrUsername: 'ایمیل یا نام کاربری', + invalidEmailOrUsername: 'ایمیل یا نام کاربری نامعتبر است', + invalidPassword: 'رمز عبور نامعتبر است', + logInToPlanka: 'ورود به Planka', + noInternetConnection: 'بدون اتصال به اینترنت', + pageNotFound_title: 'صفحه یافت نشد', + password: 'رمز عبور', + projectManagement: 'مدیریت پروژه', + serverConnectionFailed: 'اتصال به سرور ناموفق بود', + unknownError: 'خطای ناشناخته، بعداً دوباره تلاش کنید', + useSingleSignOn: 'استفاده از ورود یکپارچه', + }, + + action: { + logIn: 'ورود', + logInWithSSO: 'ورود با SSO', + }, + }, +}; diff --git a/client/src/locales/fr/core.js b/client/src/locales/fr-FR/core.js similarity index 100% rename from client/src/locales/fr/core.js rename to client/src/locales/fr-FR/core.js diff --git a/client/src/locales/fr/index.js b/client/src/locales/fr-FR/index.js similarity index 84% rename from client/src/locales/fr/index.js rename to client/src/locales/fr-FR/index.js index 945c1bc3..58933341 100644 --- a/client/src/locales/fr/index.js +++ b/client/src/locales/fr-FR/index.js @@ -1,7 +1,7 @@ import login from './login'; export default { - language: 'fr', + language: 'fr-FR', country: 'fr', name: 'Français', embeddedLocale: login, diff --git a/client/src/locales/fr/login.js b/client/src/locales/fr-FR/login.js similarity index 100% rename from client/src/locales/fr/login.js rename to client/src/locales/fr-FR/login.js diff --git a/client/src/locales/hu/core.js b/client/src/locales/hu-HU/core.js similarity index 90% rename from client/src/locales/hu/core.js rename to client/src/locales/hu-HU/core.js index 00725a1f..586b22dc 100644 --- a/client/src/locales/hu/core.js +++ b/client/src/locales/hu-HU/core.js @@ -15,6 +15,16 @@ export default { translation: { common: { + aboutPlanka: 'Plankáról', + account: 'Fiók', + actions: 'Műveletek', + addAttachment_title: 'Melléklet hozzáadása', + addComment: 'Megjegyzés hozzáadása', + addManager_title: 'Menedzser hozzáadása', + addMember_title: 'Tag létrehozása', + addUser_title: 'Felhasználó hozzáadása', + administrator: 'Rendszergazda', + all: 'Összes', allChangesWillBeAutomaticallySavedAfterConnectionRestored: 'Az összes változás automatikusan mentésre kerül a kapcsolat helyreállása után.', areYouSureYouWantToDeleteThisAttachment: 'Biztosan törölni szeretné ezt a mellékletet?', @@ -29,7 +39,7 @@ export default { areYouSureYouWantToLeaveBoard: 'Biztosan el akarja hagyni a táblát?', areYouSureYouWantToLeaveProject: 'Biztosan el akarja hagyni a projektet?', areYouSureYouWantToRemoveThisManagerFromProject: - 'Biztosan eltávolítja ezt a vezetőt a projektből?', + 'Biztosan eltávolítja ezt a menedzsert a projektből?', areYouSureYouWantToRemoveThisMemberFromBoard: 'Biztosan eltávolítja ezt a tagot a tábláról?', attachment: 'Melléklet', attachments: 'Mellékletek', @@ -44,6 +54,7 @@ export default { cardNotFound_title: 'Kártya nem található', cardOrActionAreDeleted: 'A kártya vagy művelet törölve lett.', color: 'Szín', + copy_inline: 'másolás', createBoard_title: 'Tábla létrehozása', createLabel_title: 'Címke létrehozása', createNewOneOrSelectExistingOne: 'Hozzon létre egy újat, vagy válasszon ki egy meglévőt.', @@ -52,6 +63,7 @@ export default { currentPassword: 'Jelenlegi jelszó', dangerZone_title: 'Veszélyzóna', date: 'Dátum', + dueDate: 'Határidő', dueDate_title: 'Esedékesség dátuma', deleteAttachment_title: 'Melléklet törlése', deleteBoard_title: 'Tábla törlése', @@ -97,13 +109,17 @@ export default { language: 'Nyelv', leaveBoard_title: 'Tábla elhagyása', leaveProject_title: 'Projekt elhagyása', + linkIsCopied: 'Link másolva', list: 'Lista', listActions_title: 'Lista műveletek', - managers: 'Vezetők', + managers: 'Menedzserek', + managerActions_title: 'Menedzseri műveletek', members: 'Tagok', + memberActions_title: 'Tag műveletek', minutes: 'Percek', moveCard_title: 'Kártya áthelyezése', name: 'Név', + newestFirst: 'Újabbak előre', newEmail: 'Új e-mail', newPassword: 'Új jelszó', newUsername: 'Új felhasználónév', @@ -113,6 +129,7 @@ export default { noProjects: 'Nincsenek projektek', notifications: 'Értesítések', noUnreadNotifications: 'Nincsenek olvasatlan értesítések.', + oldestFirst: 'Régebbi előre', openBoard_title: 'Tábla megnyitása', optional_inline: 'opcionális', organization: 'Szervezet', @@ -122,17 +139,19 @@ export default { 'Tipp: nyomja meg a Ctrl-V (Cmd-V a Mac-en) billentyűkombinációt a vágólapról történő melléklet hozzáadásához.', project: 'Projekt', projectNotFound_title: 'Projekt nem található', - removeManager_title: 'Vezető eltávolítása', + removeManager_title: 'Menedzser eltávolítása', removeMember_title: 'Tag eltávolítása', searchLabels: 'Címkék keresése...', searchMembers: 'Tagok keresése...', searchUsers: 'Felhasználók keresése...', + searchCards: 'Kártyák keresése..', seconds: 'Másodpercek', selectBoard: 'Válassza ki a táblát', selectList: 'Válassza ki a listát', selectPermissions_title: 'Jogosultságok kiválasztása', selectProject: 'Válassza ki a projektet', settings: 'Beállítások', + sortList_title: 'Rendezés listában', stopwatch: 'Stopper', subscribeToMyOwnCardsByDefault: 'Alapértelmezés szerint feliratkozás a saját kártyáimra', taskActions_title: 'Feladat műveletek', @@ -170,6 +189,7 @@ export default { addTask: 'Feladat hozzáadása', addToCard: 'Kártyához hozzáadása', addUser: 'Felhasználó hozzáadása', + copyLink_title: 'Link másolása', createBoard: 'Tábla létrehozása', createFile: 'Fájl létrehozása', createLabel: 'Címke létrehozása', @@ -191,6 +211,8 @@ export default { deleteTask: 'Feladat törlése', deleteTask_title: 'Feladat törlése', deleteUser: 'Felhasználó törlése', + duplicate: 'Másolás', + duplicateCard_title: 'Kártya másolása', edit: 'Szerkesztés', editDueDate_title: 'Esedékesség dátumának szerkesztése', editDescription_title: 'Leírás szerkesztése', @@ -214,12 +236,13 @@ export default { removeCover_title: 'Borító eltávolítása', removeFromBoard: 'Eltávolítás a tábláról', removeFromProject: 'Eltávolítás a projektről', - removeManager: 'Vezető eltávolítása', + removeManager: 'Menedzser eltávolítása', removeMember: 'Tag eltávolítása', save: 'Mentés', showAllAttachments: 'Összes melléklet megjelenítése ({{hidden}} rejtve)', showDetails: 'Részletek megjelenítése', showFewerAttachments: 'Kevesebb melléklet megjelenítése', + sortList_title: 'Rendezés listában', start: 'Indítás', stop: 'Megállítás', subscribe: 'Feliratkozás', diff --git a/client/src/locales/hu/index.js b/client/src/locales/hu-HU/index.js similarity index 83% rename from client/src/locales/hu/index.js rename to client/src/locales/hu-HU/index.js index 17e618ee..8b0ccdb5 100644 --- a/client/src/locales/hu/index.js +++ b/client/src/locales/hu-HU/index.js @@ -1,7 +1,7 @@ import login from './login'; export default { - language: 'hu', + language: 'hu-HU', country: 'hu', name: 'Magyar', embeddedLocale: login, diff --git a/client/src/locales/hu/login.js b/client/src/locales/hu-HU/login.js similarity index 100% rename from client/src/locales/hu/login.js rename to client/src/locales/hu-HU/login.js diff --git a/client/src/locales/id/core.js b/client/src/locales/id-ID/core.js similarity index 100% rename from client/src/locales/id/core.js rename to client/src/locales/id-ID/core.js diff --git a/client/src/locales/id/index.js b/client/src/locales/id-ID/index.js similarity index 85% rename from client/src/locales/id/index.js rename to client/src/locales/id-ID/index.js index 42801f31..e739c696 100644 --- a/client/src/locales/id/index.js +++ b/client/src/locales/id-ID/index.js @@ -1,7 +1,7 @@ import login from './login'; export default { - language: 'id', + language: 'id-ID', country: 'id', name: 'Bahasa Indonesia', embeddedLocale: login, diff --git a/client/src/locales/id/login.js b/client/src/locales/id-ID/login.js similarity index 100% rename from client/src/locales/id/login.js rename to client/src/locales/id-ID/login.js diff --git a/client/src/locales/index.js b/client/src/locales/index.js index c689d3ea..bcc7eea3 100644 --- a/client/src/locales/index.js +++ b/client/src/locales/index.js @@ -1,51 +1,53 @@ -import bg from './bg'; -import cs from './cs'; -import da from './da'; -import de from './de'; -import en from './en'; -import es from './es'; -import fr from './fr'; -import hu from './hu'; -import id from './id'; -import it from './it'; -import ja from './ja'; -import ko from './ko'; -import nl from './nl'; -import pl from './pl'; -import pt from './pt'; -import ro from './ro'; -import ru from './ru'; -import sk from './sk'; -import sv from './sv'; -import tr from './tr'; -import ua from './ua'; -import uz from './uz'; -import zh from './zh'; +import bgBG from './bg-BG'; +import csCZ from './cs-CZ'; +import daDK from './da-DK'; +import deDE from './de-DE'; +import enUS from './en-US'; +import esES from './es-ES'; +import faIR from './fa-IR'; +import frFR from './fr-FR'; +import huHU from './hu-HU'; +import idID from './id-ID'; +import itIT from './it-IT'; +import jaJP from './ja-JP'; +import koKR from './ko-KR'; +import nlNL from './nl-NL'; +import plPL from './pl-PL'; +import ptBR from './pt-BR'; +import roRO from './ro-RO'; +import ruRU from './ru-RU'; +import skSK from './sk-SK'; +import svSE from './sv-SE'; +import trTR from './tr-TR'; +import ukUA from './uk-UA'; +import uzUZ from './uz-UZ'; +import zhCN from './zh-CN'; const locales = [ - bg, - cs, - da, - de, - en, - es, - fr, - hu, - id, - it, - ja, - ko, - nl, - pl, - pt, - ro, - ru, - sk, - sv, - tr, - ua, - uz, - zh, + bgBG, + csCZ, + daDK, + deDE, + enUS, + esES, + faIR, + frFR, + huHU, + idID, + itIT, + jaJP, + koKR, + nlNL, + plPL, + ptBR, + roRO, + ruRU, + skSK, + svSE, + trTR, + ukUA, + uzUZ, + zhCN, ]; export default locales; diff --git a/client/src/locales/it/core.js b/client/src/locales/it-IT/core.js similarity index 99% rename from client/src/locales/it/core.js rename to client/src/locales/it-IT/core.js index 1afb8926..f503c6d0 100644 --- a/client/src/locales/it/core.js +++ b/client/src/locales/it-IT/core.js @@ -1,4 +1,8 @@ +import dateFns from 'date-fns/locale/it'; + export default { + dateFns, + format: { date: 'd/M/yyyy', time: 'p', diff --git a/client/src/locales/it/index.js b/client/src/locales/it-IT/index.js similarity index 84% rename from client/src/locales/it/index.js rename to client/src/locales/it-IT/index.js index bc01113b..8f64a9a6 100644 --- a/client/src/locales/it/index.js +++ b/client/src/locales/it-IT/index.js @@ -1,7 +1,7 @@ import login from './login'; export default { - language: 'it', + language: 'it-IT', country: 'it', name: 'Italiano', embeddedLocale: login, diff --git a/client/src/locales/it/login.js b/client/src/locales/it-IT/login.js similarity index 100% rename from client/src/locales/it/login.js rename to client/src/locales/it-IT/login.js diff --git a/client/src/locales/ja/core.js b/client/src/locales/ja-JP/core.js similarity index 100% rename from client/src/locales/ja/core.js rename to client/src/locales/ja-JP/core.js diff --git a/client/src/locales/ja/index.js b/client/src/locales/ja-JP/index.js similarity index 84% rename from client/src/locales/ja/index.js rename to client/src/locales/ja-JP/index.js index b33d7c46..a196e3ff 100644 --- a/client/src/locales/ja/index.js +++ b/client/src/locales/ja-JP/index.js @@ -1,7 +1,7 @@ import login from './login'; export default { - language: 'ja', + language: 'ja-JP', country: 'jp', name: '日本語', embeddedLocale: login, diff --git a/client/src/locales/ja/login.js b/client/src/locales/ja-JP/login.js similarity index 100% rename from client/src/locales/ja/login.js rename to client/src/locales/ja-JP/login.js diff --git a/client/src/locales/ko/core.js b/client/src/locales/ko-KR/core.js similarity index 100% rename from client/src/locales/ko/core.js rename to client/src/locales/ko-KR/core.js diff --git a/client/src/locales/ko/index.js b/client/src/locales/ko-KR/index.js similarity index 84% rename from client/src/locales/ko/index.js rename to client/src/locales/ko-KR/index.js index d0eb9bf0..55970aa5 100644 --- a/client/src/locales/ko/index.js +++ b/client/src/locales/ko-KR/index.js @@ -1,7 +1,7 @@ import login from './login'; export default { - language: 'ko', + language: 'ko-KR', country: 'kr', name: '한국어', embeddedLocale: login, diff --git a/client/src/locales/ko/login.js b/client/src/locales/ko-KR/login.js similarity index 100% rename from client/src/locales/ko/login.js rename to client/src/locales/ko-KR/login.js diff --git a/client/src/locales/nl/core.js b/client/src/locales/nl-NL/core.js similarity index 100% rename from client/src/locales/nl/core.js rename to client/src/locales/nl-NL/core.js diff --git a/client/src/locales/nl/index.js b/client/src/locales/nl-NL/index.js similarity index 84% rename from client/src/locales/nl/index.js rename to client/src/locales/nl-NL/index.js index 6e24c664..f7f03eb4 100644 --- a/client/src/locales/nl/index.js +++ b/client/src/locales/nl-NL/index.js @@ -1,7 +1,7 @@ import login from './login'; export default { - language: 'nl', + language: 'nl-NL', country: 'nl', name: 'Nederlands', embeddedLocale: login, diff --git a/client/src/locales/nl/login.js b/client/src/locales/nl-NL/login.js similarity index 100% rename from client/src/locales/nl/login.js rename to client/src/locales/nl-NL/login.js diff --git a/client/src/locales/pl/core.js b/client/src/locales/pl-PL/core.js similarity index 100% rename from client/src/locales/pl/core.js rename to client/src/locales/pl-PL/core.js diff --git a/client/src/locales/pl/index.js b/client/src/locales/pl-PL/index.js similarity index 83% rename from client/src/locales/pl/index.js rename to client/src/locales/pl-PL/index.js index 73a9a66b..33ab45c7 100644 --- a/client/src/locales/pl/index.js +++ b/client/src/locales/pl-PL/index.js @@ -1,7 +1,7 @@ import login from './login'; export default { - language: 'pl', + language: 'pl-PL', country: 'pl', name: 'Polski', embeddedLocale: login, diff --git a/client/src/locales/pl/login.js b/client/src/locales/pl-PL/login.js similarity index 100% rename from client/src/locales/pl/login.js rename to client/src/locales/pl-PL/login.js diff --git a/client/src/locales/pt/core.js b/client/src/locales/pt-BR/core.js similarity index 100% rename from client/src/locales/pt/core.js rename to client/src/locales/pt-BR/core.js diff --git a/client/src/locales/pt/index.js b/client/src/locales/pt-BR/index.js similarity index 84% rename from client/src/locales/pt/index.js rename to client/src/locales/pt-BR/index.js index b3c867f7..56bac4c7 100644 --- a/client/src/locales/pt/index.js +++ b/client/src/locales/pt-BR/index.js @@ -1,7 +1,7 @@ import login from './login'; export default { - language: 'pt', + language: 'pt-BR', country: 'br', name: 'Português', embeddedLocale: login, diff --git a/client/src/locales/pt/login.js b/client/src/locales/pt-BR/login.js similarity index 100% rename from client/src/locales/pt/login.js rename to client/src/locales/pt-BR/login.js diff --git a/client/src/locales/ro/core.js b/client/src/locales/ro-RO/core.js similarity index 100% rename from client/src/locales/ro/core.js rename to client/src/locales/ro-RO/core.js diff --git a/client/src/locales/ro/index.js b/client/src/locales/ro-RO/index.js similarity index 84% rename from client/src/locales/ro/index.js rename to client/src/locales/ro-RO/index.js index b10ed81d..b34d44ab 100644 --- a/client/src/locales/ro/index.js +++ b/client/src/locales/ro-RO/index.js @@ -1,7 +1,7 @@ import login from './login'; export default { - language: 'ro', + language: 'ro-RO', country: 'ro', name: 'Română', embeddedLocale: login, diff --git a/client/src/locales/ro/login.js b/client/src/locales/ro-RO/login.js similarity index 100% rename from client/src/locales/ro/login.js rename to client/src/locales/ro-RO/login.js diff --git a/client/src/locales/ru/core.js b/client/src/locales/ru-RU/core.js similarity index 100% rename from client/src/locales/ru/core.js rename to client/src/locales/ru-RU/core.js diff --git a/client/src/locales/ru/index.js b/client/src/locales/ru-RU/index.js similarity index 84% rename from client/src/locales/ru/index.js rename to client/src/locales/ru-RU/index.js index 1ffb7e0c..8c3b7914 100644 --- a/client/src/locales/ru/index.js +++ b/client/src/locales/ru-RU/index.js @@ -1,7 +1,7 @@ import login from './login'; export default { - language: 'ru', + language: 'ru-RU', country: 'ru', name: 'Русский', embeddedLocale: login, diff --git a/client/src/locales/ru/login.js b/client/src/locales/ru-RU/login.js similarity index 100% rename from client/src/locales/ru/login.js rename to client/src/locales/ru-RU/login.js diff --git a/client/src/locales/sk/core.js b/client/src/locales/sk-SK/core.js similarity index 100% rename from client/src/locales/sk/core.js rename to client/src/locales/sk-SK/core.js diff --git a/client/src/locales/sk/index.js b/client/src/locales/sk-SK/index.js similarity index 84% rename from client/src/locales/sk/index.js rename to client/src/locales/sk-SK/index.js index ad9c4046..3a0837b1 100644 --- a/client/src/locales/sk/index.js +++ b/client/src/locales/sk-SK/index.js @@ -1,7 +1,7 @@ import login from './login'; export default { - language: 'sk', + language: 'sk-SK', country: 'sk', name: 'Slovenčina', embeddedLocale: login, diff --git a/client/src/locales/sk/login.js b/client/src/locales/sk-SK/login.js similarity index 100% rename from client/src/locales/sk/login.js rename to client/src/locales/sk-SK/login.js diff --git a/client/src/locales/sv/core.js b/client/src/locales/sv-SE/core.js similarity index 100% rename from client/src/locales/sv/core.js rename to client/src/locales/sv-SE/core.js diff --git a/client/src/locales/sv/index.js b/client/src/locales/sv-SE/index.js similarity index 84% rename from client/src/locales/sv/index.js rename to client/src/locales/sv-SE/index.js index d3b043d0..d5459e59 100644 --- a/client/src/locales/sv/index.js +++ b/client/src/locales/sv-SE/index.js @@ -1,7 +1,7 @@ import login from './login'; export default { - language: 'sv', + language: 'sv-SE', country: 'se', name: 'Svenska', embeddedLocale: login, diff --git a/client/src/locales/sv/login.js b/client/src/locales/sv-SE/login.js similarity index 100% rename from client/src/locales/sv/login.js rename to client/src/locales/sv-SE/login.js diff --git a/client/src/locales/tr/core.js b/client/src/locales/tr-TR/core.js similarity index 100% rename from client/src/locales/tr/core.js rename to client/src/locales/tr-TR/core.js diff --git a/client/src/locales/tr/index.js b/client/src/locales/tr-TR/index.js similarity index 84% rename from client/src/locales/tr/index.js rename to client/src/locales/tr-TR/index.js index 5af65ff1..79e6c4ba 100644 --- a/client/src/locales/tr/index.js +++ b/client/src/locales/tr-TR/index.js @@ -1,7 +1,7 @@ import login from './login'; export default { - language: 'tr', + language: 'tr-TR', country: 'tr', name: 'Türkçe', embeddedLocale: login, diff --git a/client/src/locales/tr/login.js b/client/src/locales/tr-TR/login.js similarity index 100% rename from client/src/locales/tr/login.js rename to client/src/locales/tr-TR/login.js diff --git a/client/src/locales/ua/core.js b/client/src/locales/uk-UA/core.js similarity index 99% rename from client/src/locales/ua/core.js rename to client/src/locales/uk-UA/core.js index d1860325..eb3063a5 100644 --- a/client/src/locales/ua/core.js +++ b/client/src/locales/uk-UA/core.js @@ -1,4 +1,8 @@ +import dateFns from 'date-fns/locale/uk'; + export default { + dateFns, + format: { date: 'd/M/yyyy', time: 'p', diff --git a/client/src/locales/ua/index.js b/client/src/locales/uk-UA/index.js similarity index 85% rename from client/src/locales/ua/index.js rename to client/src/locales/uk-UA/index.js index 0190368d..1255afcb 100644 --- a/client/src/locales/ua/index.js +++ b/client/src/locales/uk-UA/index.js @@ -1,7 +1,7 @@ import login from './login'; export default { - language: 'ua', + language: 'uk-UA', country: 'ua', name: 'Українська', embeddedLocale: login, diff --git a/client/src/locales/ua/login.js b/client/src/locales/uk-UA/login.js similarity index 100% rename from client/src/locales/ua/login.js rename to client/src/locales/uk-UA/login.js diff --git a/client/src/locales/uz/core.js b/client/src/locales/uz-UZ/core.js similarity index 99% rename from client/src/locales/uz/core.js rename to client/src/locales/uz-UZ/core.js index 084de78e..f2d080c8 100644 --- a/client/src/locales/uz/core.js +++ b/client/src/locales/uz-UZ/core.js @@ -1,4 +1,8 @@ +import dateFns from 'date-fns/locale/uz'; + export default { + dateFns, + format: { date: 'M/d/yyyy', time: 'p', diff --git a/client/src/locales/uz/index.js b/client/src/locales/uz-UZ/index.js similarity index 83% rename from client/src/locales/uz/index.js rename to client/src/locales/uz-UZ/index.js index 6ecec3c6..a023b970 100644 --- a/client/src/locales/uz/index.js +++ b/client/src/locales/uz-UZ/index.js @@ -1,7 +1,7 @@ import login from './login'; export default { - language: 'uz', + language: 'uz-UZ', country: 'uz', name: "O'zbek", embeddedLocale: login, diff --git a/client/src/locales/uz/login.js b/client/src/locales/uz-UZ/login.js similarity index 100% rename from client/src/locales/uz/login.js rename to client/src/locales/uz-UZ/login.js diff --git a/client/src/locales/zh/core.js b/client/src/locales/zh-CN/core.js similarity index 99% rename from client/src/locales/zh/core.js rename to client/src/locales/zh-CN/core.js index 95ae46c3..58e03d86 100644 --- a/client/src/locales/zh/core.js +++ b/client/src/locales/zh-CN/core.js @@ -1,4 +1,8 @@ +import dateFns from 'date-fns/locale/zh-CN'; + export default { + dateFns, + format: { date: 'M/d/yyyy', time: 'p', diff --git a/client/src/locales/zh/index.js b/client/src/locales/zh-CN/index.js similarity index 83% rename from client/src/locales/zh/index.js rename to client/src/locales/zh-CN/index.js index aa5c15eb..892340e8 100644 --- a/client/src/locales/zh/index.js +++ b/client/src/locales/zh-CN/index.js @@ -1,7 +1,7 @@ import login from './login'; export default { - language: 'zh', + language: 'zh-CN', country: 'cn', name: '中文', embeddedLocale: login, diff --git a/client/src/locales/zh/login.js b/client/src/locales/zh-CN/login.js similarity index 100% rename from client/src/locales/zh/login.js rename to client/src/locales/zh-CN/login.js diff --git a/client/src/models/Attachment.js b/client/src/models/Attachment.js index ca171f8e..de3b157b 100644 --- a/client/src/models/Attachment.js +++ b/client/src/models/Attachment.js @@ -28,6 +28,7 @@ export default class extends BaseModel { case ActionTypes.CORE_INITIALIZE: case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE: case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE: + case ActionTypes.CARD_UPDATE_HANDLE: if (payload.attachments) { payload.attachments.forEach((attachment) => { Attachment.upsert(attachment); diff --git a/client/src/models/Board.js b/client/src/models/Board.js index 8e9fef2d..cdb8a62b 100755 --- a/client/src/models/Board.js +++ b/client/src/models/Board.js @@ -1,3 +1,4 @@ +import orderBy from 'lodash/orderBy'; import { attr, fk, many } from 'redux-orm'; import BaseModel from './BaseModel'; @@ -218,10 +219,6 @@ export default class extends BaseModel { } } - getOrderedMembershipsQuerySet() { - return this.memberships.orderBy('createdAt'); - } - getOrderedLabelsQuerySet() { return this.labels.orderBy('position'); } @@ -230,6 +227,12 @@ export default class extends BaseModel { return this.lists.orderBy('position'); } + getOrderedMembershipsModelArray() { + return orderBy(this.memberships.toModelArray(), (boardMembershipModel) => + boardMembershipModel.user.name.toLocaleLowerCase(), + ); + } + getMembershipModelForUser(userId) { return this.memberships .filter({ diff --git a/client/src/models/Card.js b/client/src/models/Card.js index 34b0b599..c0066634 100755 --- a/client/src/models/Card.js +++ b/client/src/models/Card.js @@ -180,7 +180,6 @@ export default class extends BaseModel { break; case ActionTypes.CARD_CREATE: case ActionTypes.CARD_UPDATE__SUCCESS: - case ActionTypes.CARD_UPDATE_HANDLE: Card.upsert(payload.card); break; @@ -202,8 +201,40 @@ export default class extends BaseModel { break; } - case ActionTypes.CARD_UPDATE: - Card.withId(payload.id).update(payload.data); + case ActionTypes.CARD_UPDATE: { + const cardModel = Card.withId(payload.id); + + // TODO: introduce separate action? + if (payload.data.boardId && payload.data.boardId !== cardModel.boardId) { + cardModel.deleteWithRelated(); + } else { + cardModel.update(payload.data); + } + + break; + } + case ActionTypes.CARD_UPDATE_HANDLE: + if (payload.isFetched) { + const cardModel = Card.withId(payload.card.id); + + if (cardModel) { + cardModel.deleteWithRelated(); + } + } + + Card.upsert(payload.card); + + if (payload.cardMemberships) { + payload.cardMemberships.forEach(({ cardId, userId }) => { + Card.withId(cardId).users.add(userId); + }); + } + + if (payload.cardLabels) { + payload.cardLabels.forEach(({ cardId, labelId }) => { + Card.withId(cardId).labels.add(labelId); + }); + } break; case ActionTypes.CARD_DUPLICATE: { diff --git a/client/src/models/Task.js b/client/src/models/Task.js index b990fe21..bb6b10be 100755 --- a/client/src/models/Task.js +++ b/client/src/models/Task.js @@ -27,6 +27,7 @@ export default class extends BaseModel { case ActionTypes.CORE_INITIALIZE: case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE: case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE: + case ActionTypes.CARD_UPDATE_HANDLE: if (payload.tasks) { payload.tasks.forEach((task) => { Task.upsert(task); diff --git a/client/src/models/User.js b/client/src/models/User.js index b2e99ccc..747ebc9e 100755 --- a/client/src/models/User.js +++ b/client/src/models/User.js @@ -298,7 +298,7 @@ export default class extends BaseModel { static getOrderedUndeletedQuerySet() { return this.filter({ deletedAt: null, - }).orderBy('createdAt'); + }).orderBy((user) => user.name.toLocaleLowerCase()); } getOrderedProjectManagersQuerySet() { diff --git a/client/src/sagas/core/services/cards.js b/client/src/sagas/core/services/cards.js index a5bc725b..8b5d7480 100644 --- a/client/src/sagas/core/services/cards.js +++ b/client/src/sagas/core/services/cards.js @@ -81,9 +81,30 @@ export function* updateCurrentCard(data) { yield call(updateCard, cardId, data); } -// TODO: handle card transfer export function* handleCardUpdate(card) { - yield put(actions.handleCardUpdate(card)); + let fetch = false; + if (card.boardId) { + const prevCard = yield select(selectors.selectCardById, card.id); + fetch = !prevCard || prevCard.boardId !== card.boardId; + } + + let cardMemberships; + let cardLabels; + let tasks; + let attachments; + + if (fetch) { + try { + ({ + item: card, // eslint-disable-line no-param-reassign + included: { cardMemberships, cardLabels, tasks, attachments }, + } = yield call(request, api.getCard, card.id)); + } catch (error) { + fetch = false; + } + } + + yield put(actions.handleCardUpdate(card, fetch, cardMemberships, cardLabels, tasks, attachments)); } export function* moveCard(id, listId, index = 0) { diff --git a/client/src/selectors/boards.js b/client/src/selectors/boards.js index e4e435cd..4bb02d33 100644 --- a/client/src/selectors/boards.js +++ b/client/src/selectors/boards.js @@ -55,17 +55,14 @@ export const selectMembershipsForCurrentBoard = createSelector( return boardModel; } - return boardModel - .getOrderedMembershipsQuerySet() - .toModelArray() - .map((boardMembershipModel) => ({ - ...boardMembershipModel.ref, - isPersisted: !isLocalId(boardMembershipModel.id), - user: { - ...boardMembershipModel.user.ref, - isCurrent: boardMembershipModel.user.id === currentUserId, - }, - })); + return boardModel.getOrderedMembershipsModelArray().map((boardMembershipModel) => ({ + ...boardMembershipModel.ref, + isPersisted: !isLocalId(boardMembershipModel.id), + user: { + ...boardMembershipModel.user.ref, + isCurrent: boardMembershipModel.user.id === currentUserId, + }, + })); }, ); diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index f69938e1..7a2c011a 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -1,7 +1,6 @@ version: '3.8' services: - server: build: context: ./server @@ -13,6 +12,7 @@ services: - NODE_ENV=development - DATABASE_URL=postgresql://user:password@postgres:5432/planka_db - SECRET_KEY=notsecretkey + # - TRUST_PROXY=0 # - TOKEN_EXPIRES_IN=365 # In days @@ -23,10 +23,17 @@ services: # Configure knex to accept SSL certificates # - KNEX_REJECT_UNAUTHORIZED_SSL_CERTIFICATE=false + + # - ALLOW_ALL_TO_CREATE_PROJECTS=true + # - OIDC_ISSUER= # - OIDC_CLIENT_ID= # - OIDC_CLIENT_SECRET= + # - OIDC_ID_TOKEN_SIGNED_RESPONSE_ALG= + # - OIDC_USERINFO_SIGNED_RESPONSE_ALG= # - OIDC_SCOPES=openid email profile + # - OIDC_RESPONSE_MODE=fragment + # - OIDC_USE_DEFAULT_RESPONSE_MODE=true # - OIDC_ADMIN_ROLES=admin # - OIDC_EMAIL_ATTRIBUTE=email # - OIDC_NAME_ATTRIBUTE=name @@ -45,9 +52,17 @@ services: # - SMTP_PASSWORD= # - SMTP_FROM="Demo Demo" + # Optional fields: accessToken, events, excludedEvents + # - | + # WEBHOOKS=[{ + # "url": "http://localhost:3001", + # "accessToken": "notaccesstoken", + # "events": ["cardCreate", "cardUpdate", "cardDelete"], + # "excludedEvents": ["notificationCreate", "notificationUpdate"] + # }] + # - SLACK_BOT_TOKEN= # - SLACK_CHANNEL_ID= - working_dir: /app command: ["sh", "-c", "npm run start"] depends_on: @@ -76,11 +91,11 @@ services: dockerfile: ../config/development/Dockerfile.server environment: - DATABASE_URL=postgresql://user:password@postgres:5432/planka_db + # - DEFAULT_ADMIN_EMAIL=demo@demo.demo # Do not remove if you want to prevent this user from being edited/deleted # - DEFAULT_ADMIN_PASSWORD=demo # - DEFAULT_ADMIN_NAME=Demo Demo # - DEFAULT_ADMIN_USERNAME=demo - working_dir: /app command: ["sh", "-c", "npm run db:init"] volumes: @@ -117,6 +132,5 @@ services: - server - client - volumes: db-data: diff --git a/docker-compose.yml b/docker-compose.yml index 85a5dab2..d28cfd6b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,10 +31,16 @@ services: # - DEFAULT_ADMIN_NAME=Demo Demo # - DEFAULT_ADMIN_USERNAME=demo + # - ALLOW_ALL_TO_CREATE_PROJECTS=true + # - OIDC_ISSUER= # - OIDC_CLIENT_ID= # - OIDC_CLIENT_SECRET= + # - OIDC_ID_TOKEN_SIGNED_RESPONSE_ALG= + # - OIDC_USERINFO_SIGNED_RESPONSE_ALG= # - OIDC_SCOPES=openid email profile + # - OIDC_RESPONSE_MODE=fragment + # - OIDC_USE_DEFAULT_RESPONSE_MODE=true # - OIDC_ADMIN_ROLES=admin # - OIDC_EMAIL_ATTRIBUTE=email # - OIDC_NAME_ATTRIBUTE=name @@ -53,6 +59,15 @@ services: # - SMTP_PASSWORD= # - SMTP_FROM="Demo Demo" + # Optional fields: accessToken, events, excludedEvents + # - | + # WEBHOOKS=[{ + # "url": "http://localhost:3001", + # "accessToken": "notaccesstoken", + # "events": ["cardCreate", "cardUpdate", "cardDelete"], + # "excludedEvents": ["notificationCreate", "notificationUpdate"] + # }] + # - SLACK_BOT_TOKEN= # - SLACK_CHANNEL_ID= depends_on: diff --git a/package-lock.json b/package-lock.json index ba78152c..7aaab2a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "planka", - "version": "1.17.5", + "version": "1.20.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "planka", - "version": "1.17.5", + "version": "1.20.1", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/package.json b/package.json index 51f81f7d..69234ff2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "planka", - "version": "1.17.5", + "version": "1.20.1", "private": true, "homepage": "https://plankanban.github.io/planka", "repository": { diff --git a/server/.env.sample b/server/.env.sample index b4fb6605..19cbc5c9 100644 --- a/server/.env.sample +++ b/server/.env.sample @@ -22,10 +22,16 @@ SECRET_KEY=notsecretkey # DEFAULT_ADMIN_NAME=Demo Demo # DEFAULT_ADMIN_USERNAME=demo +# ALLOW_ALL_TO_CREATE_PROJECTS=true + # OIDC_ISSUER= # OIDC_CLIENT_ID= # OIDC_CLIENT_SECRET= +# OIDC_ID_TOKEN_SIGNED_RESPONSE_ALG= +# OIDC_USERINFO_SIGNED_RESPONSE_ALG= # OIDC_SCOPES=openid email profile +# OIDC_RESPONSE_MODE=fragment +# OIDC_USE_DEFAULT_RESPONSE_MODE=true # OIDC_ADMIN_ROLES=admin # OIDC_EMAIL_ATTRIBUTE=email # OIDC_NAME_ATTRIBUTE=name @@ -43,12 +49,17 @@ SECRET_KEY=notsecretkey # SMTP_PASSWORD= # SMTP_FROM="Demo Demo" +# Optional fields: accessToken, events, excludedEvents +# WEBHOOKS='[{ +# "url": "http://localhost:3001", +# "accessToken": "notaccesstoken", +# "events": ["cardCreate", "cardUpdate", "cardDelete"], +# "excludedEvents": ["notificationCreate", "notificationUpdate"] +# }]' + # SLACK_BOT_TOKEN= # SLACK_CHANNEL_ID= -# WEBHOOK_URL= -# WEBHOOK_BEARER= - ## Do not edit this TZ=UTC diff --git a/server/api/controllers/access-tokens/exchange-using-oidc.js b/server/api/controllers/access-tokens/exchange-using-oidc.js index 469d4462..874d1d70 100644 --- a/server/api/controllers/access-tokens/exchange-using-oidc.js +++ b/server/api/controllers/access-tokens/exchange-using-oidc.js @@ -4,6 +4,9 @@ const Errors = { INVALID_CODE_OR_NONCE: { invalidCodeOrNonce: 'Invalid code or nonce', }, + INVALID_USERINFO_SIGNATURE: { + invalidUserinfoSignature: 'Invalid signature on userinfo due to client misconfiguration', + }, EMAIL_ALREADY_IN_USE: { emailAlreadyInUse: 'Email already in use', }, @@ -31,6 +34,9 @@ module.exports = { invalidCodeOrNonce: { responseType: 'unauthorized', }, + invalidUserinfoSignature: { + responseType: 'unauthorized', + }, emailAlreadyInUse: { responseType: 'conflict', }, @@ -51,6 +57,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('emailAlreadyInUse', () => Errors.EMAIL_ALREADY_IN_USE) .intercept('usernameAlreadyInUse', () => Errors.USERNAME_ALREADY_IN_USE) .intercept('missingValues', () => Errors.MISSING_VALUES); diff --git a/server/api/controllers/attachments/create.js b/server/api/controllers/attachments/create.js index 1ffe9627..03e0c8cf 100644 --- a/server/api/controllers/attachments/create.js +++ b/server/api/controllers/attachments/create.js @@ -44,12 +44,12 @@ module.exports = { async fn(inputs, exits) { const { currentUser } = this.req; - const { card, board } = await sails.helpers.cards + const { card, list, board, project } = await sails.helpers.cards .getProjectPath(inputs.cardId) .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); const boardMembership = await BoardMembership.findOne({ - boardId: card.boardId, + boardId: board.id, userId: currentUser.id, }); @@ -83,12 +83,14 @@ module.exports = { const fileData = await sails.helpers.attachments.processUploadedFile(file); const attachment = await sails.helpers.attachments.createOne.with({ + project, + board, + list, values: { ...fileData, card, creatorUser: currentUser, }, - board, requestId: inputs.requestId, request: this.req, }); diff --git a/server/api/controllers/attachments/delete.js b/server/api/controllers/attachments/delete.js index 697f749d..c0b0d666 100755 --- a/server/api/controllers/attachments/delete.js +++ b/server/api/controllers/attachments/delete.js @@ -33,7 +33,7 @@ module.exports = { .intercept('pathNotFound', () => Errors.ATTACHMENT_NOT_FOUND); let { attachment } = path; - const { card, board } = path; + const { card, list, board, project } = path; const boardMembership = await BoardMembership.findOne({ boardId: board.id, @@ -49,9 +49,12 @@ module.exports = { } attachment = await sails.helpers.attachments.deleteOne.with({ + project, board, + list, card, record: attachment, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/attachments/update.js b/server/api/controllers/attachments/update.js index 90e51bab..752d86db 100755 --- a/server/api/controllers/attachments/update.js +++ b/server/api/controllers/attachments/update.js @@ -37,7 +37,7 @@ module.exports = { .intercept('pathNotFound', () => Errors.ATTACHMENT_NOT_FOUND); let { attachment } = path; - const { board } = path; + const { card, list, board, project } = path; const boardMembership = await BoardMembership.findOne({ boardId: board.id, @@ -56,8 +56,12 @@ module.exports = { attachment = await sails.helpers.attachments.updateOne.with({ values, + project, board, + list, + card, record: attachment, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/board-memberships/create.js b/server/api/controllers/board-memberships/create.js index 7c70fa5b..f5337a66 100755 --- a/server/api/controllers/board-memberships/create.js +++ b/server/api/controllers/board-memberships/create.js @@ -48,14 +48,11 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - const { board } = await sails.helpers.boards + const { board, project } = await sails.helpers.boards .getProjectPath(inputs.boardId) .intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND); - const isProjectManager = await sails.helpers.users.isProjectManager( - currentUser.id, - board.projectId, - ); + const isProjectManager = await sails.helpers.users.isProjectManager(currentUser.id, project.id); if (!isProjectManager) { throw Errors.BOARD_NOT_FOUND; // Forbidden @@ -71,11 +68,13 @@ module.exports = { const boardMembership = await sails.helpers.boardMemberships.createOne .with({ + project, values: { ...values, board, user, }, + actorUser: currentUser, request: this.req, }) .intercept('userAlreadyBoardMember', () => Errors.USER_ALREADY_BOARD_MEMBER); diff --git a/server/api/controllers/board-memberships/delete.js b/server/api/controllers/board-memberships/delete.js index b9d6021c..48c073fd 100755 --- a/server/api/controllers/board-memberships/delete.js +++ b/server/api/controllers/board-memberships/delete.js @@ -27,7 +27,7 @@ module.exports = { .intercept('pathNotFound', () => Errors.BOARD_MEMBERSHIP_NOT_FOUND); let { boardMembership } = path; - const { project } = path; + const { board, project } = path; if (boardMembership.userId !== currentUser.id) { const isProjectManager = await sails.helpers.users.isProjectManager( @@ -42,7 +42,9 @@ module.exports = { boardMembership = await sails.helpers.boardMemberships.deleteOne.with({ project, + board, record: boardMembership, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/board-memberships/update.js b/server/api/controllers/board-memberships/update.js index 695b2b8a..d1ac0539 100644 --- a/server/api/controllers/board-memberships/update.js +++ b/server/api/controllers/board-memberships/update.js @@ -35,7 +35,7 @@ module.exports = { .intercept('pathNotFound', () => Errors.BOARD_MEMBERSHIP_NOT_FOUND); let { boardMembership } = path; - const { project } = path; + const { board, project } = path; const isProjectManager = await sails.helpers.users.isProjectManager(currentUser.id, project.id); @@ -47,7 +47,10 @@ module.exports = { boardMembership = await sails.helpers.boardMemberships.updateOne.with({ values, + project, + board, record: boardMembership, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/boards/create.js b/server/api/controllers/boards/create.js index 207180bc..990cb85a 100755 --- a/server/api/controllers/boards/create.js +++ b/server/api/controllers/boards/create.js @@ -103,7 +103,7 @@ module.exports = { project, }, import: boardImport, - user: currentUser, + actorUser: currentUser, requestId: inputs.requestId, request: this.req, }); diff --git a/server/api/controllers/boards/delete.js b/server/api/controllers/boards/delete.js index 9d0fc5cd..f834576d 100755 --- a/server/api/controllers/boards/delete.js +++ b/server/api/controllers/boards/delete.js @@ -22,25 +22,27 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - let { board } = await sails.helpers.boards + const path = await sails.helpers.boards .getProjectPath(inputs.id) .intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND); + let { board } = path; + const { project } = path; + if (!board) { throw Errors.BOARD_NOT_FOUND; } - const isProjectManager = await sails.helpers.users.isProjectManager( - currentUser.id, - board.projectId, - ); + const isProjectManager = await sails.helpers.users.isProjectManager(currentUser.id, project.id); if (!isProjectManager) { throw Errors.BOARD_NOT_FOUND; // Forbidden } board = await sails.helpers.boards.deleteOne.with({ + project, record: board, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/boards/update.js b/server/api/controllers/boards/update.js index 85c2d01c..2ffb5741 100755 --- a/server/api/controllers/boards/update.js +++ b/server/api/controllers/boards/update.js @@ -29,18 +29,18 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - let { board } = await sails.helpers.boards + const path = await sails.helpers.boards .getProjectPath(inputs.id) .intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND); + let { board } = path; + const { project } = path; + if (!board) { throw Errors.BOARD_NOT_FOUND; } - const isProjectManager = await sails.helpers.users.isProjectManager( - currentUser.id, - board.projectId, - ); + const isProjectManager = await sails.helpers.users.isProjectManager(currentUser.id, project.id); if (!isProjectManager) { throw Errors.BOARD_NOT_FOUND; // Forbidden @@ -50,7 +50,9 @@ module.exports = { board = await sails.helpers.boards.updateOne.with({ values, + project, record: board, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/card-labels/create.js b/server/api/controllers/card-labels/create.js index 207619af..68341372 100755 --- a/server/api/controllers/card-labels/create.js +++ b/server/api/controllers/card-labels/create.js @@ -45,12 +45,12 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - const { card } = await sails.helpers.cards + const { card, list, board, project } = await sails.helpers.cards .getProjectPath(inputs.cardId) .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); const boardMembership = await BoardMembership.findOne({ - boardId: card.boardId, + boardId: board.id, userId: currentUser.id, }); @@ -64,7 +64,7 @@ module.exports = { const label = await Label.findOne({ id: inputs.labelId, - boardId: card.boardId, + boardId: board.id, }); if (!label) { @@ -73,10 +73,14 @@ module.exports = { const cardLabel = await sails.helpers.cardLabels.createOne .with({ + project, + board, + list, values: { card, label, }, + actorUser: currentUser, request: this.req, }) .intercept('labelAlreadyInCard', () => Errors.LABEL_ALREADY_IN_CARD); diff --git a/server/api/controllers/card-labels/delete.js b/server/api/controllers/card-labels/delete.js index 2911bce4..23b4c9c9 100755 --- a/server/api/controllers/card-labels/delete.js +++ b/server/api/controllers/card-labels/delete.js @@ -39,7 +39,7 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - const { board } = await sails.helpers.cards + const { card, list, board, project } = await sails.helpers.cards .getProjectPath(inputs.cardId) .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); @@ -66,8 +66,12 @@ module.exports = { } cardLabel = await sails.helpers.cardLabels.deleteOne.with({ + project, board, + list, + card, record: cardLabel, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/card-memberships/create.js b/server/api/controllers/card-memberships/create.js index 9143cc91..14f544ef 100755 --- a/server/api/controllers/card-memberships/create.js +++ b/server/api/controllers/card-memberships/create.js @@ -45,12 +45,12 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - const { card, board } = await sails.helpers.cards + const { card, list, board, project } = await sails.helpers.cards .getProjectPath(inputs.cardId) .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); const boardMembership = await BoardMembership.findOne({ - boardId: card.boardId, + boardId: board.id, userId: currentUser.id, }); @@ -62,7 +62,7 @@ module.exports = { throw Errors.NOT_ENOUGH_RIGHTS; } - const isBoardMember = await sails.helpers.users.isBoardMember(inputs.userId, card.boardId); + const isBoardMember = await sails.helpers.users.isBoardMember(inputs.userId, board.id); if (!isBoardMember) { throw Errors.USER_NOT_FOUND; @@ -70,11 +70,14 @@ module.exports = { const cardMembership = await sails.helpers.cardMemberships.createOne .with({ + project, + board, + list, values: { card, userId: inputs.userId, }, - board, + actorUser: currentUser, request: this.req, }) .intercept('userAlreadyCardMember', () => Errors.USER_ALREADY_CARD_MEMBER); diff --git a/server/api/controllers/card-memberships/delete.js b/server/api/controllers/card-memberships/delete.js index 4b9d7547..297c93b4 100755 --- a/server/api/controllers/card-memberships/delete.js +++ b/server/api/controllers/card-memberships/delete.js @@ -39,7 +39,7 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - const { board, card } = await sails.helpers.cards + const { card, list, board, project } = await sails.helpers.cards .getProjectPath(inputs.cardId) .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); @@ -66,9 +66,12 @@ module.exports = { } cardMembership = await sails.helpers.cardMemberships.deleteOne.with({ + project, board, + list, card, record: cardMembership, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/cards/create.js b/server/api/controllers/cards/create.js index b847c3e0..33bc6b2d 100755 --- a/server/api/controllers/cards/create.js +++ b/server/api/controllers/cards/create.js @@ -78,7 +78,7 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - const { board, list } = await sails.helpers.lists + const { list, board, project } = await sails.helpers.lists .getProjectPath(inputs.listId) .intercept('pathNotFound', () => Errors.LIST_NOT_FOUND); @@ -99,6 +99,7 @@ module.exports = { const card = await sails.helpers.cards.createOne .with({ + project, board, values: { ...values, diff --git a/server/api/controllers/cards/delete.js b/server/api/controllers/cards/delete.js index dad82de1..12bb9a5b 100755 --- a/server/api/controllers/cards/delete.js +++ b/server/api/controllers/cards/delete.js @@ -28,12 +28,15 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - let { card } = await sails.helpers.cards + const path = await sails.helpers.cards .getProjectPath(inputs.id) .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); + let { card } = path; + const { list, board, project } = path; + const boardMembership = await BoardMembership.findOne({ - boardId: card.boardId, + boardId: board.id, userId: currentUser.id, }); @@ -46,8 +49,11 @@ module.exports = { } card = await sails.helpers.cards.deleteOne.with({ + project, + board, + list, record: card, - user: currentUser, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/cards/duplicate.js b/server/api/controllers/cards/duplicate.js index 918a8ec1..25cfaad5 100755 --- a/server/api/controllers/cards/duplicate.js +++ b/server/api/controllers/cards/duplicate.js @@ -35,12 +35,12 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - const { card, list, board } = await sails.helpers.cards + const { card, list, board, project } = await sails.helpers.cards .getProjectPath(inputs.id) .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); const boardMembership = await BoardMembership.findOne({ - boardId: card.boardId, + boardId: board.id, userId: currentUser.id, }); @@ -60,6 +60,7 @@ module.exports = { cardLabels, tasks, } = await sails.helpers.cards.duplicateOne.with({ + project, board, list, record: card, diff --git a/server/api/controllers/cards/update.js b/server/api/controllers/cards/update.js index 8ee37523..d3deaf48 100755 --- a/server/api/controllers/cards/update.js +++ b/server/api/controllers/cards/update.js @@ -118,7 +118,7 @@ module.exports = { .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); let { card } = path; - const { list, board } = path; + const { list, board, project } = path; let boardMembership = await BoardMembership.findOne({ boardId: board.id, @@ -133,9 +133,11 @@ module.exports = { throw Errors.NOT_ENOUGH_RIGHTS; } + let nextProject; let nextBoard; + if (!_.isUndefined(inputs.boardId)) { - ({ board: nextBoard } = await sails.helpers.boards + ({ board: nextBoard, project: nextProject } = await sails.helpers.boards .getProjectPath(inputs.boardId) .intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND)); @@ -177,15 +179,17 @@ module.exports = { card = await sails.helpers.cards.updateOne .with({ + project, board, list, record: card, values: { ...values, + project: nextProject, board: nextBoard, list: nextList, }, - user: currentUser, + actorUser: currentUser, request: this.req, }) .intercept('positionMustBeInValues', () => Errors.POSITION_MUST_BE_PRESENT) diff --git a/server/api/controllers/comment-actions/create.js b/server/api/controllers/comment-actions/create.js index 7712a3a5..41cc694d 100755 --- a/server/api/controllers/comment-actions/create.js +++ b/server/api/controllers/comment-actions/create.js @@ -32,7 +32,7 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - const { board, card } = await sails.helpers.cards + const { card, list, board, project } = await sails.helpers.cards .getProjectPath(inputs.cardId) .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); @@ -55,7 +55,9 @@ module.exports = { }; const action = await sails.helpers.actions.createOne.with({ + project, board, + list, values: { ...values, card, diff --git a/server/api/controllers/comment-actions/delete.js b/server/api/controllers/comment-actions/delete.js index 97c98790..25082358 100755 --- a/server/api/controllers/comment-actions/delete.js +++ b/server/api/controllers/comment-actions/delete.js @@ -36,7 +36,7 @@ module.exports = { .intercept('pathNotFound', () => Errors.COMMENT_ACTION_NOT_FOUND); let { action } = path; - const { board, project, card } = path; + const { card, list, board, project } = path; const isProjectManager = await sails.helpers.users.isProjectManager(currentUser.id, project.id); @@ -60,9 +60,12 @@ module.exports = { } action = await sails.helpers.actions.deleteOne.with({ + project, board, + list, card, record: action, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/comment-actions/update.js b/server/api/controllers/comment-actions/update.js index a385ae8c..aa62fd5c 100755 --- a/server/api/controllers/comment-actions/update.js +++ b/server/api/controllers/comment-actions/update.js @@ -40,7 +40,7 @@ module.exports = { .intercept('pathNotFound', () => Errors.COMMENT_ACTION_NOT_FOUND); let { action } = path; - const { board, project, card } = path; + const { card, list, board, project } = path; const isProjectManager = await sails.helpers.users.isProjectManager(currentUser.id, project.id); @@ -69,9 +69,12 @@ module.exports = { action = await sails.helpers.actions.updateOne.with({ values, - card, + project, board, + list, + card, record: action, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/labels/create.js b/server/api/controllers/labels/create.js index d3b8743c..a1e84991 100755 --- a/server/api/controllers/labels/create.js +++ b/server/api/controllers/labels/create.js @@ -42,7 +42,7 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - const { board } = await sails.helpers.boards + const { board, project } = await sails.helpers.boards .getProjectPath(inputs.boardId) .intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND); @@ -62,10 +62,12 @@ module.exports = { const values = _.pick(inputs, ['position', 'name', 'color']); const label = await sails.helpers.labels.createOne.with({ + project, values: { ...values, board, }, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/labels/delete.js b/server/api/controllers/labels/delete.js index e2f0a098..f14bf451 100755 --- a/server/api/controllers/labels/delete.js +++ b/server/api/controllers/labels/delete.js @@ -28,12 +28,15 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - let { label } = await sails.helpers.labels + const path = await sails.helpers.labels .getProjectPath(inputs.id) .intercept('pathNotFound', () => Errors.LABEL_NOT_FOUND); + let { label } = path; + const { board, project } = path; + const boardMembership = await BoardMembership.findOne({ - boardId: label.boardId, + boardId: board.id, userId: currentUser.id, }); @@ -46,7 +49,10 @@ module.exports = { } label = await sails.helpers.labels.deleteOne.with({ + project, + board, record: label, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/labels/update.js b/server/api/controllers/labels/update.js index e289bdf9..7e76b74b 100755 --- a/server/api/controllers/labels/update.js +++ b/server/api/controllers/labels/update.js @@ -40,12 +40,15 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - let { label } = await sails.helpers.labels + const path = await sails.helpers.labels .getProjectPath(inputs.id) .intercept('pathNotFound', () => Errors.LABEL_NOT_FOUND); + let { label } = path; + const { board, project } = path; + const boardMembership = await BoardMembership.findOne({ - boardId: label.boardId, + boardId: board.id, userId: currentUser.id, }); @@ -61,7 +64,10 @@ module.exports = { label = await sails.helpers.labels.updateOne.with({ values, + project, + board, record: label, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/lists/create.js b/server/api/controllers/lists/create.js index 9383f5be..869c00bb 100755 --- a/server/api/controllers/lists/create.js +++ b/server/api/controllers/lists/create.js @@ -36,7 +36,7 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - const { board } = await sails.helpers.boards + const { board, project } = await sails.helpers.boards .getProjectPath(inputs.boardId) .intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND); @@ -56,10 +56,12 @@ module.exports = { const values = _.pick(inputs, ['position', 'name']); const list = await sails.helpers.lists.createOne.with({ + project, values: { ...values, board, }, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/lists/delete.js b/server/api/controllers/lists/delete.js index 982b0f73..2368161f 100755 --- a/server/api/controllers/lists/delete.js +++ b/server/api/controllers/lists/delete.js @@ -28,13 +28,15 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - // eslint-disable-next-line prefer-const - let { list, board } = await sails.helpers.lists + const path = await sails.helpers.lists .getProjectPath(inputs.id) .intercept('pathNotFound', () => Errors.LIST_NOT_FOUND); + let { list } = path; + const { board, project } = path; + const boardMembership = await BoardMembership.findOne({ - boardId: list.boardId, + boardId: board.id, userId: currentUser.id, }); @@ -47,8 +49,10 @@ module.exports = { } list = await sails.helpers.lists.deleteOne.with({ - record: list, + project, board, + record: list, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/lists/sort.js b/server/api/controllers/lists/sort.js index ec5863f6..0451ca69 100644 --- a/server/api/controllers/lists/sort.js +++ b/server/api/controllers/lists/sort.js @@ -32,12 +32,12 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - const { list } = await sails.helpers.lists + const { list, board, project } = await sails.helpers.lists .getProjectPath(inputs.id) .intercept('pathNotFound', () => Errors.LIST_NOT_FOUND); const boardMembership = await BoardMembership.findOne({ - boardId: list.boardId, + boardId: board.id, userId: currentUser.id, }); @@ -50,8 +50,11 @@ module.exports = { } const cards = await sails.helpers.lists.sortOne.with({ + project, + board, record: list, type: inputs.type, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/lists/update.js b/server/api/controllers/lists/update.js index 15f5f209..e1988b93 100755 --- a/server/api/controllers/lists/update.js +++ b/server/api/controllers/lists/update.js @@ -35,13 +35,15 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - // eslint-disable-next-line prefer-const - let { list, board } = await sails.helpers.lists + const path = await sails.helpers.lists .getProjectPath(inputs.id) .intercept('pathNotFound', () => Errors.LIST_NOT_FOUND); + let { list } = path; + const { board, project } = path; + const boardMembership = await BoardMembership.findOne({ - boardId: list.boardId, + boardId: board.id, userId: currentUser.id, }); @@ -57,8 +59,10 @@ module.exports = { list = await sails.helpers.lists.updateOne.with({ values, + project, board, record: list, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/notifications/update.js b/server/api/controllers/notifications/update.js index f8a97a35..09d5b4c2 100755 --- a/server/api/controllers/notifications/update.js +++ b/server/api/controllers/notifications/update.js @@ -18,7 +18,7 @@ module.exports = { const notifications = await sails.helpers.notifications.updateMany.with({ values, recordsOrIds: inputs.ids.split(','), - user: currentUser, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/project-managers/create.js b/server/api/controllers/project-managers/create.js index a5751fdb..e3796958 100755 --- a/server/api/controllers/project-managers/create.js +++ b/server/api/controllers/project-managers/create.js @@ -63,6 +63,7 @@ module.exports = { project, user, }, + actorUser: currentUser, request: this.req, }) .intercept('userAlreadyProjectManager', () => Errors.USER_ALREADY_PROJECT_MANAGER); diff --git a/server/api/controllers/project-managers/delete.js b/server/api/controllers/project-managers/delete.js index 5bd3f9af..46946e61 100755 --- a/server/api/controllers/project-managers/delete.js +++ b/server/api/controllers/project-managers/delete.js @@ -40,6 +40,7 @@ module.exports = { // TODO: check if the last one projectManager = await sails.helpers.projectManagers.deleteOne.with({ record: projectManager, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/projects/create.js b/server/api/controllers/projects/create.js index 1ce57ae8..54bd7f00 100755 --- a/server/api/controllers/projects/create.js +++ b/server/api/controllers/projects/create.js @@ -1,3 +1,9 @@ +const Errors = { + NOT_ENOUGH_RIGHTS: { + notEnoughRights: 'Not enough rights', + }, +}; + module.exports = { inputs: { name: { @@ -6,14 +12,24 @@ module.exports = { }, }, + exits: { + notEnoughRights: { + responseType: 'forbidden', + }, + }, + async fn(inputs) { const { currentUser } = this.req; + if (!currentUser.isAdmin && !sails.config.custom.allowAllToCreateProjects) { + throw Errors.NOT_ENOUGH_RIGHTS; + } + const values = _.pick(inputs, ['name']); const { project, projectManager } = await sails.helpers.projects.createOne.with({ values, - user: currentUser, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/projects/delete.js b/server/api/controllers/projects/delete.js index dedb9070..90ae180d 100755 --- a/server/api/controllers/projects/delete.js +++ b/server/api/controllers/projects/delete.js @@ -36,6 +36,7 @@ module.exports = { project = await sails.helpers.projects.deleteOne.with({ record: project, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/projects/update-background-image.js b/server/api/controllers/projects/update-background-image.js index 85825e08..2045ee1e 100755 --- a/server/api/controllers/projects/update-background-image.js +++ b/server/api/controllers/projects/update-background-image.js @@ -90,6 +90,7 @@ module.exports = { values: { backgroundImage: fileData, }, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/projects/update.js b/server/api/controllers/projects/update.js index 6d6e16e9..230be644 100755 --- a/server/api/controllers/projects/update.js +++ b/server/api/controllers/projects/update.js @@ -81,6 +81,7 @@ module.exports = { project = await sails.helpers.projects.updateOne.with({ values, record: project, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/show-config.js b/server/api/controllers/show-config.js index 5349149a..1a8207ec 100644 --- a/server/api/controllers/show-config.js +++ b/server/api/controllers/show-config.js @@ -4,11 +4,16 @@ module.exports = { if (sails.hooks.oidc.isActive()) { const oidcClient = sails.hooks.oidc.getClient(); + const authorizationUrlParams = { + scope: sails.config.custom.oidcScopes, + }; + + if (!sails.config.custom.oidcUseDefaultResponseMode) { + authorizationUrlParams.response_mode = sails.config.custom.oidcResponseMode; + } + oidc = { - authorizationUrl: oidcClient.authorizationUrl({ - scope: sails.config.custom.oidcScopes, - response_mode: 'fragment', - }), + authorizationUrl: oidcClient.authorizationUrl(authorizationUrlParams), endSessionUrl: oidcClient.issuer.end_session_endpoint ? oidcClient.endSessionUrl({}) : null, isEnforced: sails.config.custom.oidcEnforced, }; @@ -17,6 +22,7 @@ module.exports = { return { item: { oidc, + allowAllToCreateProjects: sails.config.custom.allowAllToCreateProjects, }, }; }, diff --git a/server/api/controllers/tasks/create.js b/server/api/controllers/tasks/create.js index ff4b0523..126e39a6 100755 --- a/server/api/controllers/tasks/create.js +++ b/server/api/controllers/tasks/create.js @@ -39,12 +39,12 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - const { card, board } = await sails.helpers.cards + const { card, list, board, project } = await sails.helpers.cards .getProjectPath(inputs.cardId) .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); const boardMembership = await BoardMembership.findOne({ - boardId: card.boardId, + boardId: board.id, userId: currentUser.id, }); @@ -59,11 +59,14 @@ module.exports = { const values = _.pick(inputs, ['position', 'name', 'isCompleted']); const task = await sails.helpers.tasks.createOne.with({ + project, + board, + list, values: { ...values, card, }, - board, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/tasks/delete.js b/server/api/controllers/tasks/delete.js index c4ba0ed3..a83cc9dc 100755 --- a/server/api/controllers/tasks/delete.js +++ b/server/api/controllers/tasks/delete.js @@ -33,7 +33,7 @@ module.exports = { .intercept('pathNotFound', () => Errors.TASK_NOT_FOUND); let { task } = path; - const { board } = path; + const { card, list, board, project } = path; const boardMembership = await BoardMembership.findOne({ boardId: board.id, @@ -49,8 +49,12 @@ module.exports = { } task = await sails.helpers.tasks.deleteOne.with({ + project, board, + list, + card, record: task, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/tasks/update.js b/server/api/controllers/tasks/update.js index 9e08f76f..ff43fcb8 100755 --- a/server/api/controllers/tasks/update.js +++ b/server/api/controllers/tasks/update.js @@ -43,7 +43,7 @@ module.exports = { .intercept('pathNotFound', () => Errors.TASK_NOT_FOUND); let { task } = path; - const { board } = path; + const { card, list, board, project } = path; const boardMembership = await BoardMembership.findOne({ boardId: board.id, @@ -62,8 +62,12 @@ module.exports = { task = await sails.helpers.tasks.updateOne.with({ values, + project, board, + list, + card, record: task, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/users/create.js b/server/api/controllers/users/create.js index 7d40c975..dc76b31f 100755 --- a/server/api/controllers/users/create.js +++ b/server/api/controllers/users/create.js @@ -50,7 +50,7 @@ module.exports = { }, language: { type: 'string', - isNotEmptyString: true, + isIn: User.LANGUAGES, allowNull: true, }, subscribeToOwnCards: { @@ -71,6 +71,8 @@ module.exports = { }, async fn(inputs) { + const { currentUser } = this.req; + if (sails.config.custom.oidcEnforced) { throw Errors.NOT_ENOUGH_RIGHTS; } @@ -89,6 +91,7 @@ module.exports = { const user = await sails.helpers.users.createOne .with({ values, + actorUser: currentUser, request: this.req, }) .intercept('emailAlreadyInUse', () => Errors.EMAIL_ALREADY_IN_USE) diff --git a/server/api/controllers/users/delete.js b/server/api/controllers/users/delete.js index 56a42f8b..b1afab29 100755 --- a/server/api/controllers/users/delete.js +++ b/server/api/controllers/users/delete.js @@ -26,6 +26,8 @@ module.exports = { }, async fn(inputs) { + const { currentUser } = this.req; + let user = await sails.helpers.users.getOne(inputs.id); if (!user) { @@ -38,6 +40,7 @@ module.exports = { user = await sails.helpers.users.deleteOne.with({ record: user, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/users/update-avatar.js b/server/api/controllers/users/update-avatar.js index 37b01f11..fc628564 100755 --- a/server/api/controllers/users/update-avatar.js +++ b/server/api/controllers/users/update-avatar.js @@ -91,7 +91,7 @@ module.exports = { values: { avatar: fileData, }, - user: currentUser, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/users/update-email.js b/server/api/controllers/users/update-email.js index f067e004..17575577 100644 --- a/server/api/controllers/users/update-email.js +++ b/server/api/controllers/users/update-email.js @@ -82,7 +82,7 @@ module.exports = { .with({ values, record: user, - user: currentUser, + actorUser: currentUser, request: this.req, }) .intercept('emailAlreadyInUse', () => Errors.EMAIL_ALREADY_IN_USE); diff --git a/server/api/controllers/users/update-password.js b/server/api/controllers/users/update-password.js index 107c013e..01466811 100644 --- a/server/api/controllers/users/update-password.js +++ b/server/api/controllers/users/update-password.js @@ -80,7 +80,7 @@ module.exports = { user = await sails.helpers.users.updateOne.with({ values, record: user, - user: currentUser, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/controllers/users/update-username.js b/server/api/controllers/users/update-username.js index b55529b4..a59c587d 100644 --- a/server/api/controllers/users/update-username.js +++ b/server/api/controllers/users/update-username.js @@ -83,7 +83,7 @@ module.exports = { .with({ values, record: user, - user: currentUser, + actorUser: currentUser, request: this.req, }) .intercept('usernameAlreadyInUse', () => Errors.USERNAME_ALREADY_IN_USE); diff --git a/server/api/controllers/users/update.js b/server/api/controllers/users/update.js index 161ae78c..a46a2f7f 100755 --- a/server/api/controllers/users/update.js +++ b/server/api/controllers/users/update.js @@ -36,7 +36,7 @@ module.exports = { }, language: { type: 'string', - isNotEmptyString: true, + isIn: User.LANGUAGES, allowNull: true, }, subscribeToOwnCards: { @@ -95,7 +95,7 @@ module.exports = { user = await sails.helpers.users.updateOne.with({ values, record: user, - user: currentUser, + actorUser: currentUser, request: this.req, }); diff --git a/server/api/helpers/actions/create-one.js b/server/api/helpers/actions/create-one.js index e0310c82..fb0d1856 100644 --- a/server/api/helpers/actions/create-one.js +++ b/server/api/helpers/actions/create-one.js @@ -14,21 +14,21 @@ const valuesValidator = (value) => { return true; }; -const buildAndSendSlackMessage = async (user, card, action) => { +const buildAndSendSlackMessage = async (card, action, actorUser) => { const cardLink = `<${sails.config.custom.baseUrl}/cards/${card.id}|${card.name}>`; let markdown; switch (action.type) { case Action.Types.CREATE_CARD: - markdown = `${cardLink} was created by ${user.name} in *${action.data.list.name}*`; + markdown = `${cardLink} was created by ${actorUser.name} in *${action.data.list.name}*`; break; case Action.Types.MOVE_CARD: - markdown = `${cardLink} was moved by ${user.name} to *${action.data.toList.name}*`; + markdown = `${cardLink} was moved by ${actorUser.name} to *${action.data.toList.name}*`; break; case Action.Types.COMMENT_CARD: - markdown = `*${user.name}* commented on ${cardLink}:\n>${action.data.text}`; + markdown = `*${actorUser.name}* commented on ${cardLink}:\n>${action.data.text}`; break; default: @@ -45,10 +45,18 @@ module.exports = { custom: valuesValidator, required: true, }, + project: { + type: 'ref', + required: true, + }, board: { type: 'ref', required: true, }, + list: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -72,6 +80,24 @@ module.exports = { inputs.request, ); + sails.helpers.utils.sendWebhooks.with({ + event: 'actionCreate', + data: { + item: action, + included: { + projects: [inputs.project], + boards: [inputs.board], + lists: [inputs.list], + cards: [values.card], + }, + }, + user: values.user, + }); + + if (sails.config.custom.slackBotToken) { + buildAndSendSlackMessage(values.card, action, values.user); + } + const subscriptionUserIds = await sails.helpers.cards.getSubscriptionUserIds( action.cardId, action.userId, @@ -84,26 +110,15 @@ module.exports = { userId, action, }, - user: values.user, + project: inputs.project, board: inputs.board, + list: inputs.list, card: values.card, + actorUser: values.user, }), ), ); - if (sails.config.custom.slackBotToken) { - buildAndSendSlackMessage(values.user, values.card, action); - } - - await sails.helpers.utils.sendWebhook.with({ - event: 'ACTION_CREATE', - data: action, - projectId: inputs.board.projectId, - user: inputs.request.currentUser, - card: values.card, - board: inputs.board, - }); - return action; }, }; diff --git a/server/api/helpers/actions/delete-one.js b/server/api/helpers/actions/delete-one.js index ba862f85..3ccaff19 100644 --- a/server/api/helpers/actions/delete-one.js +++ b/server/api/helpers/actions/delete-one.js @@ -4,11 +4,23 @@ module.exports = { type: 'ref', required: true, }, + project: { + type: 'ref', + required: true, + }, + board: { + type: 'ref', + required: true, + }, + list: { + type: 'ref', + required: true, + }, card: { type: 'ref', required: true, }, - board: { + actorUser: { type: 'ref', required: true, }, @@ -30,13 +42,18 @@ module.exports = { inputs.request, ); - await sails.helpers.utils.sendWebhook.with({ - event: 'ACTION_DELETE', - data: action, - projectId: inputs.board.projectId, - user: inputs.request.currentUser, - card: inputs.card, - board: inputs.board, + sails.helpers.utils.sendWebhooks.with({ + event: 'actionDelete', + data: { + item: action, + included: { + projects: [inputs.project], + boards: [inputs.board], + lists: [inputs.list], + cards: [inputs.card], + }, + }, + user: inputs.actorUser, }); } diff --git a/server/api/helpers/actions/update-one.js b/server/api/helpers/actions/update-one.js index cdca79c0..7835fa74 100644 --- a/server/api/helpers/actions/update-one.js +++ b/server/api/helpers/actions/update-one.js @@ -8,7 +8,7 @@ module.exports = { type: 'json', required: true, }, - card: { + project: { type: 'ref', required: true, }, @@ -16,6 +16,18 @@ module.exports = { type: 'ref', required: true, }, + list: { + type: 'ref', + required: true, + }, + card: { + type: 'ref', + required: true, + }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -36,13 +48,18 @@ module.exports = { inputs.request, ); - await sails.helpers.utils.sendWebhook.with({ - event: 'ACTION_UPDATE', - data: action, - projectId: inputs.board.projectId, - user: inputs.request.currentUser, - card: inputs.card, - board: inputs.board, + sails.helpers.utils.sendWebhooks.with({ + event: 'actionUpdate', + data: { + item: action, + included: { + projects: [inputs.project], + boards: [inputs.board], + lists: [inputs.list], + cards: [inputs.card], + }, + }, + user: inputs.actorUser, }); } diff --git a/server/api/helpers/attachments/create-one.js b/server/api/helpers/attachments/create-one.js index 0a5923b3..8db35ab6 100644 --- a/server/api/helpers/attachments/create-one.js +++ b/server/api/helpers/attachments/create-one.js @@ -21,10 +21,18 @@ module.exports = { custom: valuesValidator, required: true, }, + project: { + type: 'ref', + required: true, + }, board: { type: 'ref', required: true, }, + list: { + type: 'ref', + required: true, + }, requestId: { type: 'string', isNotEmptyString: true, @@ -35,7 +43,7 @@ module.exports = { }, async fn(inputs) { - const { values, board } = inputs; + const { values } = inputs; const attachment = await Attachment.create({ ...values, @@ -53,26 +61,33 @@ module.exports = { inputs.request, ); + sails.helpers.utils.sendWebhooks.with({ + event: 'attachmentCreate', + data: { + item: attachment, + included: { + projects: [inputs.project], + boards: [inputs.board], + lists: [inputs.list], + cards: [values.card], + }, + }, + user: values.creatorUser, + }); + if (!values.card.coverAttachmentId && attachment.image) { await sails.helpers.cards.updateOne.with({ record: values.card, values: { coverAttachmentId: attachment.id, }, - board, - request: inputs.request, + project: inputs.project, + board: inputs.board, + list: inputs.list, + actorUser: values.creatorUser, }); } - await sails.helpers.utils.sendWebhook.with({ - event: 'ATTACHMENT_CREATE', - data: attachment, - projectId: board.projectId, - user: inputs.request.currentUser, - card: values.card, - board, - }); - return attachment; }, }; diff --git a/server/api/helpers/attachments/delete-one.js b/server/api/helpers/attachments/delete-one.js index 469e1461..9d08cb4f 100644 --- a/server/api/helpers/attachments/delete-one.js +++ b/server/api/helpers/attachments/delete-one.js @@ -7,14 +7,26 @@ module.exports = { type: 'ref', required: true, }, + project: { + type: 'ref', + required: true, + }, board: { type: 'ref', required: true, }, + list: { + type: 'ref', + required: true, + }, card: { type: 'ref', required: true, }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -27,6 +39,10 @@ module.exports = { values: { coverAttachmentId: null, }, + project: inputs.project, + board: inputs.board, + list: inputs.list, + actorUser: inputs.actorUser, request: inputs.request, }); } @@ -49,13 +65,18 @@ module.exports = { inputs.request, ); - await sails.helpers.utils.sendWebhook.with({ - event: 'ATTACHMENT_DELETE', - data: attachment, - projectId: inputs.board.projectId, - user: inputs.request.currentUser, - card: inputs.card, - board: inputs.board, + sails.helpers.utils.sendWebhooks.with({ + event: 'attachmentDelete', + data: { + item: attachment, + included: { + projects: [inputs.project], + boards: [inputs.board], + lists: [inputs.list], + cards: [inputs.card], + }, + }, + user: inputs.actorUser, }); } diff --git a/server/api/helpers/attachments/update-one.js b/server/api/helpers/attachments/update-one.js index 9de735e1..49660206 100644 --- a/server/api/helpers/attachments/update-one.js +++ b/server/api/helpers/attachments/update-one.js @@ -8,10 +8,26 @@ module.exports = { type: 'json', required: true, }, + project: { + type: 'ref', + required: true, + }, board: { type: 'ref', required: true, }, + list: { + type: 'ref', + required: true, + }, + card: { + type: 'ref', + required: true, + }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -32,12 +48,18 @@ module.exports = { inputs.request, ); - await sails.helpers.utils.sendWebhook.with({ - event: 'ATTACHMENT_UPDATE', - data: attachment, - projectId: inputs.board.projectId, - user: inputs.request.currentUser, - board: inputs.board, + sails.helpers.utils.sendWebhooks.with({ + event: 'attachmentUpdate', + data: { + item: attachment, + included: { + projects: [inputs.project], + boards: [inputs.board], + lists: [inputs.list], + cards: [inputs.card], + }, + }, + user: inputs.actorUser, }); } diff --git a/server/api/helpers/board-memberships/create-one.js b/server/api/helpers/board-memberships/create-one.js index 5adf9eb2..f6088474 100644 --- a/server/api/helpers/board-memberships/create-one.js +++ b/server/api/helpers/board-memberships/create-one.js @@ -21,6 +21,14 @@ module.exports = { custom: valuesValidator, required: true, }, + project: { + type: 'ref', + required: true, + }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -67,6 +75,19 @@ module.exports = { inputs.request, ); + sails.helpers.utils.sendWebhooks.with({ + event: 'boardMembershipCreate', + data: { + item: boardMembership, + included: { + users: [values.user], + projects: [inputs.project], + boards: [values.board], + }, + }, + user: inputs.actorUser, + }); + return boardMembership; }, }; diff --git a/server/api/helpers/board-memberships/delete-one.js b/server/api/helpers/board-memberships/delete-one.js index 236734ab..e51daf4d 100644 --- a/server/api/helpers/board-memberships/delete-one.js +++ b/server/api/helpers/board-memberships/delete-one.js @@ -10,6 +10,14 @@ module.exports = { type: 'ref', required: true, }, + board: { + type: 'ref', + required: true, + }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -73,6 +81,18 @@ module.exports = { ); }); } + + sails.helpers.utils.sendWebhooks.with({ + event: 'boardMembershipDelete', + data: { + item: boardMembership, + included: { + projects: [inputs.project], + boards: [inputs.board], + }, + }, + user: inputs.actorUser, + }); } return boardMembership; diff --git a/server/api/helpers/board-memberships/update-one.js b/server/api/helpers/board-memberships/update-one.js index cb43adc8..63948907 100644 --- a/server/api/helpers/board-memberships/update-one.js +++ b/server/api/helpers/board-memberships/update-one.js @@ -8,6 +8,18 @@ module.exports = { type: 'json', required: true, }, + project: { + type: 'ref', + required: true, + }, + board: { + type: 'ref', + required: true, + }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -40,6 +52,18 @@ module.exports = { }, inputs.request, ); + + sails.helpers.utils.sendWebhooks.with({ + event: 'boardMembershipUpdate', + data: { + item: boardMembership, + included: { + projects: [inputs.project], + boards: [inputs.board], + }, + }, + user: inputs.actorUser, + }); } return boardMembership; diff --git a/server/api/helpers/boards/create-one.js b/server/api/helpers/boards/create-one.js index 98618722..368dda92 100644 --- a/server/api/helpers/boards/create-one.js +++ b/server/api/helpers/boards/create-one.js @@ -37,7 +37,7 @@ module.exports = { type: 'json', custom: importValidator, }, - user: { + actorUser: { type: 'ref', required: true, }, @@ -80,6 +80,8 @@ module.exports = { position: nextPosition, }, }); + + // TODO: send webhooks }); }); @@ -90,12 +92,12 @@ module.exports = { }).fetch(); if (inputs.import && inputs.import.type === Board.ImportTypes.TRELLO) { - await sails.helpers.boards.importFromTrello(inputs.user, board, inputs.import.board); + await sails.helpers.boards.importFromTrello(board, inputs.import.board, inputs.actorUser); } const boardMembership = await BoardMembership.create({ boardId: board.id, - userId: inputs.user.id, + userId: inputs.actorUser.id, role: BoardMembership.Roles.EDITOR, }).fetch(); @@ -111,12 +113,15 @@ module.exports = { ); }); - await sails.helpers.utils.sendWebhook.with({ - event: 'BOARD_CREATE', - data: board, - projectId: board.projectId, - user: inputs.request.currentUser, - board, + sails.helpers.utils.sendWebhooks.with({ + event: 'boardCreate', + data: { + item: board, + included: { + projects: [values.project], + }, + }, + user: inputs.actorUser, }); return { diff --git a/server/api/helpers/boards/delete-one.js b/server/api/helpers/boards/delete-one.js index ddc51d7e..8633cd55 100644 --- a/server/api/helpers/boards/delete-one.js +++ b/server/api/helpers/boards/delete-one.js @@ -4,6 +4,14 @@ module.exports = { type: 'ref', required: true, }, + project: { + type: 'ref', + required: true, + }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -33,15 +41,18 @@ module.exports = { inputs.request, ); }); - } - await sails.helpers.utils.sendWebhook.with({ - event: 'BOARD_DELETE', - data: board, - projectId: board.projectId, - user: inputs.request.currentUser, - board, - }); + sails.helpers.utils.sendWebhooks.with({ + event: 'boardDelete', + data: { + item: board, + included: { + projects: [inputs.project], + }, + }, + user: inputs.actorUser, + }); + } return board; }, diff --git a/server/api/helpers/boards/import-from-trello.js b/server/api/helpers/boards/import-from-trello.js index 2ab6e0af..a7b54238 100644 --- a/server/api/helpers/boards/import-from-trello.js +++ b/server/api/helpers/boards/import-from-trello.js @@ -2,10 +2,6 @@ const POSITION_GAP = 65535; // TODO: move to config module.exports = { inputs: { - user: { - type: 'ref', - required: true, - }, board: { type: 'ref', required: true, @@ -14,6 +10,10 @@ module.exports = { type: 'json', required: true, }, + actorUser: { + type: 'ref', + required: true, + }, }, async fn(inputs) { @@ -87,7 +87,7 @@ module.exports = { trelloComments.map(async (trelloComment) => { return Action.create({ cardId: plankaCard.id, - userId: inputs.user.id, + userId: inputs.actorUser.id, type: 'commentCard', data: { text: @@ -105,7 +105,7 @@ module.exports = { const plankaCard = await Card.create({ boardId: inputs.board.id, listId: plankaList.id, - creatorUserId: inputs.user.id, + creatorUserId: inputs.actorUser.id, position: trelloCard.pos, name: trelloCard.name, description: trelloCard.desc || null, diff --git a/server/api/helpers/boards/update-one.js b/server/api/helpers/boards/update-one.js index 82fdcce0..e20324dd 100644 --- a/server/api/helpers/boards/update-one.js +++ b/server/api/helpers/boards/update-one.js @@ -21,6 +21,14 @@ module.exports = { custom: valuesValidator, required: true, }, + project: { + type: 'ref', + required: true, + }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -64,6 +72,8 @@ module.exports = { position: nextPosition, }, }); + + // TODO: send webhooks }); }); } @@ -81,15 +91,18 @@ module.exports = { inputs.request, ); }); - } - await sails.helpers.utils.sendWebhook.with({ - event: 'BOARD_UPDATE', - data: board, - projectId: board.projectId, - user: inputs.request.currentUser, - board, - }); + sails.helpers.utils.sendWebhooks.with({ + event: 'boardUpdate', + data: { + item: board, + included: { + projects: [inputs.project], + }, + }, + user: inputs.actorUser, + }); + } return board; }, diff --git a/server/api/helpers/card-labels/create-one.js b/server/api/helpers/card-labels/create-one.js index f0cde82c..7e168ca9 100644 --- a/server/api/helpers/card-labels/create-one.js +++ b/server/api/helpers/card-labels/create-one.js @@ -21,6 +21,22 @@ module.exports = { custom: valuesValidator, required: true, }, + project: { + type: 'ref', + required: true, + }, + board: { + type: 'ref', + required: true, + }, + list: { + type: 'ref', + required: true, + }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -50,6 +66,21 @@ module.exports = { inputs.request, ); + sails.helpers.utils.sendWebhooks.with({ + event: 'cardLabelCreate', + data: { + item: cardLabel, + included: { + projects: [inputs.project], + boards: [inputs.board], + labels: [values.label], + lists: [inputs.list], + cards: [values.card], + }, + }, + user: inputs.actorUser, + }); + return cardLabel; }, }; diff --git a/server/api/helpers/card-labels/delete-one.js b/server/api/helpers/card-labels/delete-one.js index 3dc30ddc..e557d383 100644 --- a/server/api/helpers/card-labels/delete-one.js +++ b/server/api/helpers/card-labels/delete-one.js @@ -4,10 +4,26 @@ module.exports = { type: 'ref', required: true, }, + project: { + type: 'ref', + required: true, + }, board: { type: 'ref', required: true, }, + list: { + type: 'ref', + required: true, + }, + card: { + type: 'ref', + required: true, + }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -25,6 +41,20 @@ module.exports = { }, inputs.request, ); + + sails.helpers.utils.sendWebhooks.with({ + event: 'cardLabelDelete', + data: { + item: cardLabel, + included: { + projects: [inputs.project], + boards: [inputs.board], + lists: [inputs.list], + cards: [inputs.card], + }, + }, + user: inputs.actorUser, + }); } return cardLabel; diff --git a/server/api/helpers/card-memberships/create-one.js b/server/api/helpers/card-memberships/create-one.js index 52f17882..c32ff428 100644 --- a/server/api/helpers/card-memberships/create-one.js +++ b/server/api/helpers/card-memberships/create-one.js @@ -21,10 +21,22 @@ module.exports = { custom: valuesValidator, required: true, }, + project: { + type: 'ref', + required: true, + }, board: { type: 'ref', required: true, }, + list: { + type: 'ref', + required: true, + }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -57,6 +69,20 @@ module.exports = { inputs.request, ); + sails.helpers.utils.sendWebhooks.with({ + event: 'cardMembershipCreate', + data: { + item: cardMembership, + included: { + projects: [inputs.project], + boards: [inputs.board], + lists: [inputs.list], + cards: [values.card], + }, + }, + user: inputs.actorUser, + }); + const cardSubscription = await CardSubscription.create({ cardId: cardMembership.cardId, userId: cardMembership.userId, @@ -77,16 +103,9 @@ module.exports = { }, inputs.request, ); - } - await sails.helpers.utils.sendWebhook.with({ - event: 'CARD_MEMBERSHIP_CREATE', - data: cardMembership, - projectId: inputs.board.projectId, - user: inputs.request.currentUser, - card: values.card, - board: inputs.board, - }); + // TODO: send webhooks + } return cardMembership; }, diff --git a/server/api/helpers/card-memberships/delete-one.js b/server/api/helpers/card-memberships/delete-one.js index a4ed107f..cfa2fc7e 100644 --- a/server/api/helpers/card-memberships/delete-one.js +++ b/server/api/helpers/card-memberships/delete-one.js @@ -4,11 +4,23 @@ module.exports = { type: 'ref', required: true, }, + project: { + type: 'ref', + required: true, + }, + board: { + type: 'ref', + required: true, + }, + list: { + type: 'ref', + required: true, + }, card: { type: 'ref', required: true, }, - board: { + actorUser: { type: 'ref', required: true, }, @@ -30,6 +42,20 @@ module.exports = { inputs.request, ); + sails.helpers.utils.sendWebhooks.with({ + event: 'cardMembershipDelete', + data: { + item: cardMembership, + included: { + projects: [inputs.project], + boards: [inputs.board], + lists: [inputs.list], + cards: [inputs.card], + }, + }, + user: inputs.actorUser, + }); + const cardSubscription = await CardSubscription.destroyOne({ cardId: cardMembership.cardId, userId: cardMembership.userId, @@ -43,16 +69,9 @@ module.exports = { isSubscribed: false, }, }); - } - await sails.helpers.utils.sendWebhook.with({ - event: 'CARD_MEMBERSHIP_DELETE', - data: cardMembership, - projectId: inputs.board.projectId, - user: inputs.request.currentUser, - card: inputs.card, - board: inputs.board, - }); + // TODO: send webhook + } } return cardMembership; diff --git a/server/api/helpers/cards/create-one.js b/server/api/helpers/cards/create-one.js index 0514bf5f..36cdb81a 100644 --- a/server/api/helpers/cards/create-one.js +++ b/server/api/helpers/cards/create-one.js @@ -25,6 +25,10 @@ module.exports = { custom: valuesValidator, required: true, }, + project: { + type: 'ref', + required: true, + }, board: { type: 'ref', required: true, @@ -66,6 +70,8 @@ module.exports = { position: nextPosition, }, }); + + // TODO: send webhooks }); const card = await Card.create({ @@ -85,6 +91,19 @@ module.exports = { inputs.request, ); + sails.helpers.utils.sendWebhooks.with({ + event: 'cardCreate', + data: { + item: card, + included: { + projects: [inputs.project], + boards: [inputs.board], + lists: [values.list], + }, + }, + user: values.creatorUser, + }); + if (values.creatorUser.subscribeToOwnCards) { await CardSubscription.create({ cardId: card.id, @@ -97,6 +116,8 @@ module.exports = { isSubscribed: true, }, }); + + // TODO: send webhooks } await sails.helpers.actions.createOne.with({ @@ -108,18 +129,10 @@ module.exports = { }, user: values.creatorUser, }, - board: inputs.board, - request: inputs.request, - }); - - await sails.helpers.utils.sendWebhook.with({ - event: 'CARD_CREATE', - data: card, - projectId: inputs.board.projectId, - user: inputs.request.currentUser, - card, + project: inputs.project, board: inputs.board, list: values.list, + request: inputs.request, }); return card; diff --git a/server/api/helpers/cards/delete-one.js b/server/api/helpers/cards/delete-one.js index e95596e8..1400bfcb 100644 --- a/server/api/helpers/cards/delete-one.js +++ b/server/api/helpers/cards/delete-one.js @@ -1,5 +1,5 @@ -const buildAndSendSlackMessage = async (user, card) => { - await sails.helpers.utils.sendSlackMessage(`*${card.name}* was deleted by ${user.name}`); +const buildAndSendSlackMessage = async (card, actorUser) => { + await sails.helpers.utils.sendSlackMessage(`*${card.name}* was deleted by ${actorUser.name}`); }; module.exports = { @@ -8,7 +8,19 @@ module.exports = { type: 'ref', required: true, }, - user: { + project: { + type: 'ref', + required: true, + }, + board: { + type: 'ref', + required: true, + }, + list: { + type: 'ref', + required: true, + }, + actorUser: { type: 'ref', required: true, }, @@ -21,10 +33,6 @@ module.exports = { const card = await Card.archiveOne(inputs.record.id); if (card) { - const { board } = await sails.helpers.lists - .getProjectPath(card.listId) - .intercept('pathNotFound', () => Errors.LIST_NOT_FOUND); - sails.sockets.broadcast( `board:${card.boardId}`, 'cardDelete', @@ -34,18 +42,22 @@ module.exports = { inputs.request, ); - if (sails.config.custom.slackBotToken) { - buildAndSendSlackMessage(inputs.user, card); - } - - await sails.helpers.utils.sendWebhook.with({ - event: 'CARD_DELETE', - data: card, - projectId: board.projectId, - user: inputs.request.currentUser, - card, - board, + sails.helpers.utils.sendWebhooks.with({ + event: 'cardDelete', + data: { + item: card, + included: { + projects: [inputs.project], + boards: [inputs.board], + lists: [inputs.list], + }, + }, + user: inputs.actorUser, }); + + if (sails.config.custom.slackBotToken) { + buildAndSendSlackMessage(card, inputs.actorUser); + } } return card; diff --git a/server/api/helpers/cards/duplicate-one.js b/server/api/helpers/cards/duplicate-one.js index 71645533..94a0d755 100644 --- a/server/api/helpers/cards/duplicate-one.js +++ b/server/api/helpers/cards/duplicate-one.js @@ -25,6 +25,10 @@ module.exports = { custom: valuesValidator, required: true, }, + project: { + type: 'ref', + required: true, + }, board: { type: 'ref', required: true, @@ -62,6 +66,8 @@ module.exports = { position: nextPosition, }, }); + + // TODO: send webhooks }); const card = await Card.create({ @@ -108,6 +114,19 @@ module.exports = { inputs.request, ); + sails.helpers.utils.sendWebhooks.with({ + event: 'cardCreate', + data: { + item: card, + included: { + projects: [inputs.project], + boards: [inputs.board], + lists: [inputs.list], + }, + }, + user: values.creatorUser, + }); + if (values.creatorUser.subscribeToOwnCards) { await CardSubscription.create({ cardId: card.id, @@ -120,6 +139,8 @@ module.exports = { isSubscribed: true, }, }); + + // TODO: send webhooks } await sails.helpers.actions.createOne.with({ @@ -131,18 +152,10 @@ module.exports = { }, user: values.creatorUser, }, - board: inputs.board, - request: inputs.request, - }); - - await sails.helpers.utils.sendWebhook.with({ - event: 'CARD_CREATE', - data: card, - projectId: inputs.board.projectId, - user: inputs.request.currentUser, - card, + project: inputs.project, board: inputs.board, list: inputs.list, + request: inputs.request, }); return { diff --git a/server/api/helpers/cards/update-one.js b/server/api/helpers/cards/update-one.js index cc054039..cd978b7e 100644 --- a/server/api/helpers/cards/update-one.js +++ b/server/api/helpers/cards/update-one.js @@ -7,8 +7,14 @@ const valuesValidator = (value) => { return false; } - if (!_.isUndefined(value.board) && !_.isPlainObject(value.board)) { - return false; + if (!_.isUndefined(value.board)) { + if (!_.isPlainObject(value.project)) { + return false; + } + + if (!_.isPlainObject(value.board)) { + return false; + } } if (!_.isUndefined(value.list) && !_.isPlainObject(value.list)) { @@ -29,14 +35,21 @@ module.exports = { custom: valuesValidator, required: true, }, - user: { + project: { type: 'ref', + required: true, }, board: { type: 'ref', + required: true, }, list: { type: 'ref', + required: true, + }, + actorUser: { + type: 'ref', + required: true, }, request: { type: 'ref', @@ -45,65 +58,56 @@ module.exports = { exits: { positionMustBeInValues: {}, + boardInValuesMustBelongToProject: {}, listMustBeInValues: {}, listInValuesMustBelongToBoard: {}, - userMustBePresent: {}, - boardMustBePresent: {}, - listMustBePresent: {}, }, async fn(inputs) { const { isSubscribed, ...values } = inputs.values; - if (values.board || values.list || !_.isUndefined(values.position)) { - if (!inputs.board) { - throw 'boardMustBePresent'; + if (values.project && values.project.id === inputs.project.id) { + delete values.project; + } + + const project = values.project || inputs.project; + + if (values.board) { + if (values.board.projectId !== project.id) { + throw 'boardInValuesMustBelongToProject'; } - if (values.board) { - if (values.board.id === inputs.board.id) { - delete values.board; - } else { - values.boardId = values.board.id; - } - } - - const board = values.board || inputs.board; - - if (values.list) { - if (!inputs.list) { - throw 'listMustBePresent'; - } - - if (values.list.boardId !== board.id) { - throw 'listInValuesMustBelongToBoard'; - } - - if (values.list.id === inputs.list.id) { - delete values.list; - } else { - values.listId = values.list.id; - } - } - - if (values.list) { - if (_.isUndefined(values.position)) { - throw 'positionMustBeInValues'; - } - } else if (values.board) { - throw 'listMustBeInValues'; + if (values.board.id === inputs.board.id) { + delete values.board; + } else { + values.boardId = values.board.id; } } - if ((!_.isUndefined(isSubscribed) || values.board || values.list) && !inputs.user) { - throw 'userMustBePresent'; + const board = values.board || inputs.board; + + if (values.list) { + if (values.list.boardId !== board.id) { + throw 'listInValuesMustBelongToBoard'; + } + + if (values.list.id === inputs.list.id) { + delete values.list; + } else { + values.listId = values.list.id; + } + } else if (values.board) { + throw 'listMustBeInValues'; + } + + const list = values.list || inputs.list; + + if (values.list && _.isUndefined(values.position)) { + throw 'positionMustBeInValues'; } if (!_.isUndefined(values.position)) { - const boardId = values.boardId || inputs.record.boardId; - const listId = values.listId || inputs.record.listId; - - const cards = await sails.helpers.lists.getCards(listId, inputs.record.id); + const cards = await sails.helpers.lists.getCards(list.id, inputs.record.id); const { position, repositions } = sails.helpers.utils.insertToPositionables( values.position, @@ -115,17 +119,19 @@ module.exports = { repositions.forEach(async ({ id, position: nextPosition }) => { await Card.update({ id, - listId, + listId: list.id, }).set({ position: nextPosition, }); - sails.sockets.broadcast(`board:${boardId}`, 'cardUpdate', { + sails.sockets.broadcast(`board:${board.id}`, 'cardUpdate', { item: { id, position: nextPosition, }, }); + + // TODO: send webhooks }); } @@ -175,10 +181,12 @@ module.exports = { } const { id } = await sails.helpers.labels.createOne.with({ + project, values: { ..._.omit(label, ['id', 'boardId']), board: values.board, }, + actorUser: inputs.actorUser, }); return id; @@ -196,6 +204,15 @@ module.exports = { ), ); + sails.sockets.broadcast( + `board:${inputs.record.boardId}`, + 'cardDelete', // TODO: introduce separate event + { + item: inputs.record, + }, + inputs.request, + ); + sails.sockets.broadcast(`board:${card.boardId}`, 'cardUpdate', { item: card, }); @@ -209,6 +226,8 @@ module.exports = { isSubscribed: true, }, }); + + // TODO: send webhooks }); } else { sails.sockets.broadcast( @@ -221,18 +240,33 @@ module.exports = { ); } + sails.helpers.utils.sendWebhooks.with({ + event: 'cardUpdate', + data: { + item: card, + included: { + projects: [project], + boards: [board], + lists: [list], + }, + }, + user: inputs.actorUser, + }); + if (!values.board && values.list) { await sails.helpers.actions.createOne.with({ + project, + board, + list, values: { card, - user: inputs.user, + user: inputs.actorUser, type: Action.Types.MOVE_CARD, data: { fromList: _.pick(inputs.list, ['id', 'name']), toList: _.pick(values.list, ['id', 'name']), }, }, - board: inputs.board, request: inputs.request, }); } @@ -241,23 +275,26 @@ module.exports = { } if (!_.isUndefined(isSubscribed)) { - const prevIsSubscribed = await sails.helpers.users.isCardSubscriber(inputs.user.id, card.id); + const prevIsSubscribed = await sails.helpers.users.isCardSubscriber( + inputs.actorUser.id, + card.id, + ); if (isSubscribed !== prevIsSubscribed) { if (isSubscribed) { await CardSubscription.create({ cardId: card.id, - userId: inputs.user.id, + userId: inputs.actorUser.id, }).tolerate('E_UNIQUE'); } else { await CardSubscription.destroyOne({ cardId: card.id, - userId: inputs.user.id, + userId: inputs.actorUser.id, }); } sails.sockets.broadcast( - `user:${inputs.user.id}`, + `user:${inputs.actorUser.id}`, 'cardUpdate', { item: { @@ -267,18 +304,11 @@ module.exports = { }, inputs.request, ); + + // TODO: send webhooks } } - await sails.helpers.utils.sendWebhook.with({ - event: 'CARD_UPDATE', - data: card, - projectId: inputs.board.projectId, - user: inputs.request.currentUser, - card, - board: inputs.board, - }); - return card; }, }; diff --git a/server/api/helpers/labels/create-one.js b/server/api/helpers/labels/create-one.js index 04f58c36..f9d66d64 100644 --- a/server/api/helpers/labels/create-one.js +++ b/server/api/helpers/labels/create-one.js @@ -21,6 +21,14 @@ module.exports = { custom: valuesValidator, required: true, }, + project: { + type: 'ref', + required: true, + }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -50,6 +58,8 @@ module.exports = { position: nextPosition, }, }); + + // TODO: send webhooks }); const label = await Label.create({ @@ -67,6 +77,18 @@ module.exports = { inputs.request, ); + sails.helpers.utils.sendWebhooks.with({ + event: 'labelCreate', + data: { + item: label, + included: { + projects: [inputs.project], + boards: [values.board], + }, + }, + user: inputs.actorUser, + }); + return label; }, }; diff --git a/server/api/helpers/labels/delete-one.js b/server/api/helpers/labels/delete-one.js index 654d97c7..da45eb9c 100644 --- a/server/api/helpers/labels/delete-one.js +++ b/server/api/helpers/labels/delete-one.js @@ -4,6 +4,18 @@ module.exports = { type: 'ref', required: true, }, + project: { + type: 'ref', + required: true, + }, + board: { + type: 'ref', + required: true, + }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -25,6 +37,18 @@ module.exports = { }, inputs.request, ); + + sails.helpers.utils.sendWebhooks.with({ + event: 'labelDelete', + data: { + item: label, + included: { + projects: [inputs.project], + boards: [inputs.board], + }, + }, + user: inputs.actorUser, + }); } return label; diff --git a/server/api/helpers/labels/update-one.js b/server/api/helpers/labels/update-one.js index 3237ce8c..01efcdbd 100644 --- a/server/api/helpers/labels/update-one.js +++ b/server/api/helpers/labels/update-one.js @@ -21,6 +21,18 @@ module.exports = { custom: valuesValidator, required: true, }, + project: { + type: 'ref', + required: true, + }, + board: { + type: 'ref', + required: true, + }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -53,6 +65,8 @@ module.exports = { position: nextPosition, }, }); + + // TODO: send webhooks }); } @@ -67,6 +81,18 @@ module.exports = { }, inputs.request, ); + + sails.helpers.utils.sendWebhooks.with({ + event: 'labelUpdate', + data: { + item: label, + included: { + projects: [inputs.project], + boards: [inputs.board], + }, + }, + user: inputs.actorUser, + }); } return label; diff --git a/server/api/helpers/lists/create-one.js b/server/api/helpers/lists/create-one.js index 1c65fcce..833ff6d2 100644 --- a/server/api/helpers/lists/create-one.js +++ b/server/api/helpers/lists/create-one.js @@ -21,6 +21,14 @@ module.exports = { custom: valuesValidator, required: true, }, + project: { + type: 'ref', + required: true, + }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -50,6 +58,8 @@ module.exports = { position: nextPosition, }, }); + + // TODO: send webhooks }); const list = await List.create({ @@ -67,12 +77,16 @@ module.exports = { inputs.request, ); - await sails.helpers.utils.sendWebhook.with({ - event: 'LIST_CREATE', - data: list, - projectId: values.board.projectId, - user: inputs.request.currentUser, - board: values.board, + sails.helpers.utils.sendWebhooks.with({ + event: 'listCreate', + data: { + item: list, + included: { + projects: [inputs.project], + boards: [values.board], + }, + }, + user: inputs.actorUser, }); return list; diff --git a/server/api/helpers/lists/delete-one.js b/server/api/helpers/lists/delete-one.js index d4f7c49b..78041730 100644 --- a/server/api/helpers/lists/delete-one.js +++ b/server/api/helpers/lists/delete-one.js @@ -4,10 +4,18 @@ module.exports = { type: 'ref', required: true, }, + project: { + type: 'ref', + required: true, + }, board: { type: 'ref', required: true, }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -26,12 +34,16 @@ module.exports = { inputs.request, ); - await sails.helpers.utils.sendWebhook.with({ - event: 'LIST_DELETE', - data: list, - projectId: inputs.board.projectId, - user: inputs.request.currentUser, - board: inputs.board, + sails.helpers.utils.sendWebhooks.with({ + event: 'listDelete', + data: { + item: list, + included: { + projects: [inputs.project], + boards: [inputs.board], + }, + }, + user: inputs.actorUser, }); } diff --git a/server/api/helpers/lists/sort-one.js b/server/api/helpers/lists/sort-one.js index c5e4fbd2..58384ef6 100644 --- a/server/api/helpers/lists/sort-one.js +++ b/server/api/helpers/lists/sort-one.js @@ -11,6 +11,18 @@ module.exports = { isIn: Object.values(List.SortTypes), defaultsTo: List.SortTypes.NAME_ASC, }, + project: { + type: 'ref', + required: true, + }, + board: { + type: 'ref', + required: true, + }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -65,6 +77,19 @@ module.exports = { inputs.request, ); + sails.helpers.utils.sendWebhooks.with({ + event: 'listSort', + data: { + item: inputs.record, + included: { + cards, + projects: [inputs.project], + boards: [inputs.board], + }, + }, + user: inputs.actorUser, + }); + return cards; }, }; diff --git a/server/api/helpers/lists/update-one.js b/server/api/helpers/lists/update-one.js index ba186b82..0335b9ee 100644 --- a/server/api/helpers/lists/update-one.js +++ b/server/api/helpers/lists/update-one.js @@ -21,10 +21,18 @@ module.exports = { custom: valuesValidator, required: true, }, + project: { + type: 'ref', + required: true, + }, board: { type: 'ref', required: true, }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -57,6 +65,8 @@ module.exports = { position: nextPosition, }, }); + + // TODO: send webhooks }); } @@ -72,12 +82,16 @@ module.exports = { inputs.request, ); - await sails.helpers.utils.sendWebhook.with({ - event: 'LIST_UPDATE', - data: list, - projectId: inputs.board.projectId, - user: inputs.request.currentUser, - board: inputs.board, + sails.helpers.utils.sendWebhooks.with({ + event: 'listUpdate', + data: { + item: list, + included: { + projects: [inputs.project], + boards: [inputs.board], + }, + }, + user: inputs.actorUser, }); } diff --git a/server/api/helpers/notifications/create-one.js b/server/api/helpers/notifications/create-one.js index f24e4bfa..95d88382 100644 --- a/server/api/helpers/notifications/create-one.js +++ b/server/api/helpers/notifications/create-one.js @@ -15,14 +15,14 @@ const valuesValidator = (value) => { }; // TODO: use templates (views) to build html -const buildAndSendEmail = async (user, board, card, action, notifiableUser) => { +const buildAndSendEmail = async (board, card, action, actorUser, notifiableUser) => { let emailData; switch (action.type) { case Action.Types.MOVE_CARD: emailData = { - subject: `${user.name} moved ${card.name} from ${action.data.fromList.name} to ${action.data.toList.name} on ${board.name}`, + subject: `${actorUser.name} moved ${card.name} from ${action.data.fromList.name} to ${action.data.toList.name} on ${board.name}`, html: - `

${user.name} moved ` + + `

${actorUser.name} moved ` + `${card.name} ` + `from ${action.data.fromList.name} to ${action.data.toList.name} ` + `on ${board.name}

`, @@ -31,9 +31,9 @@ const buildAndSendEmail = async (user, board, card, action, notifiableUser) => { break; case Action.Types.COMMENT_CARD: emailData = { - subject: `${user.name} left a new comment to ${card.name} on ${board.name}`, + subject: `${actorUser.name} left a new comment to ${card.name} on ${board.name}`, html: - `

${user.name} left a new comment to ` + + `

${actorUser.name} left a new comment to ` + `${card.name} ` + `on ${board.name}

` + `

${action.data.text}

`, @@ -57,7 +57,7 @@ module.exports = { custom: valuesValidator, required: true, }, - user: { + project: { type: 'ref', required: true, }, @@ -65,10 +65,18 @@ module.exports = { type: 'ref', required: true, }, + list: { + type: 'ref', + required: true, + }, card: { type: 'ref', required: true, }, + actorUser: { + type: 'ref', + required: true, + }, }, async fn(inputs) { @@ -96,9 +104,24 @@ module.exports = { notifiableUser = await sails.helpers.users.getOne(notification.userId); } - buildAndSendEmail(inputs.user, inputs.board, inputs.card, values.action, notifiableUser); + buildAndSendEmail(inputs.board, inputs.card, values.action, inputs.actorUser, notifiableUser); } + sails.helpers.utils.sendWebhooks.with({ + event: 'notificationCreate', + data: { + item: notification, + included: { + projects: [inputs.project], + boards: [inputs.board], + lists: [inputs.list], + cards: [inputs.card], + actions: [values.action], + }, + }, + user: inputs.actorUser, + }); + return notification; }, }; diff --git a/server/api/helpers/notifications/update-many.js b/server/api/helpers/notifications/update-many.js index fee2c024..7775919d 100644 --- a/server/api/helpers/notifications/update-many.js +++ b/server/api/helpers/notifications/update-many.js @@ -12,8 +12,9 @@ module.exports = { type: 'json', required: true, }, - user: { + actorUser: { type: 'ref', + required: true, }, request: { type: 'ref', @@ -23,7 +24,9 @@ module.exports = { async fn(inputs) { const { values } = inputs; - const criteria = {}; + const criteria = { + userId: inputs.actorUser.id, + }; if (_.every(inputs.recordsOrIds, _.isPlainObject)) { criteria.id = sails.helpers.utils.mapRecords(inputs.recordsOrIds); @@ -31,10 +34,6 @@ module.exports = { criteria.id = inputs.recordsOrIds; } - if (inputs.user) { - criteria.userId = inputs.user.id; - } - const notifications = await Notification.update(criteria) .set({ ...values }) .fetch(); @@ -48,6 +47,14 @@ module.exports = { }, inputs.request, ); + + sails.helpers.utils.sendWebhooks.with({ + event: 'notificationUpdate', + data: { + item: notification, + }, + user: inputs.actorUser, + }); }); return notifications; diff --git a/server/api/helpers/project-managers/create-one.js b/server/api/helpers/project-managers/create-one.js index 56929d18..80a9a432 100644 --- a/server/api/helpers/project-managers/create-one.js +++ b/server/api/helpers/project-managers/create-one.js @@ -21,6 +21,10 @@ module.exports = { custom: valuesValidator, required: true, }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -55,6 +59,18 @@ module.exports = { ); }); + sails.helpers.utils.sendWebhooks.with({ + event: 'projectManagerCreate', + data: { + item: projectManager, + included: { + users: [values.user], + projects: [values.project], + }, + }, + user: inputs.actorUser, + }); + return projectManager; }, }; diff --git a/server/api/helpers/project-managers/delete-one.js b/server/api/helpers/project-managers/delete-one.js index 3677836d..aa9b7487 100644 --- a/server/api/helpers/project-managers/delete-one.js +++ b/server/api/helpers/project-managers/delete-one.js @@ -4,6 +4,10 @@ module.exports = { type: 'ref', required: true, }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -27,6 +31,14 @@ module.exports = { inputs.request, ); }); + + sails.helpers.utils.sendWebhooks.with({ + event: 'projectManagerDelete', + data: { + item: projectManager, + }, + user: inputs.actorUser, + }); } return projectManager; diff --git a/server/api/helpers/projects/create-one.js b/server/api/helpers/projects/create-one.js index 7deb9c4a..a22dd698 100644 --- a/server/api/helpers/projects/create-one.js +++ b/server/api/helpers/projects/create-one.js @@ -4,7 +4,7 @@ module.exports = { type: 'json', required: true, }, - user: { + actorUser: { type: 'ref', required: true, }, @@ -20,7 +20,7 @@ module.exports = { const projectManager = await ProjectManager.create({ projectId: project.id, - userId: inputs.user.id, + userId: inputs.actorUser.id, }).fetch(); sails.sockets.broadcast( @@ -32,11 +32,12 @@ module.exports = { inputs.request, ); - await sails.helpers.utils.sendWebhook.with({ - event: 'PROJECT_CREATE', - data: project, - projectId: project.id, - user: inputs.request.currentUser, + sails.helpers.utils.sendWebhooks.with({ + event: 'projectCreate', + data: { + item: project, + }, + user: inputs.actorUser, }); return { diff --git a/server/api/helpers/projects/delete-one.js b/server/api/helpers/projects/delete-one.js index 1021d272..c5b3c9dc 100644 --- a/server/api/helpers/projects/delete-one.js +++ b/server/api/helpers/projects/delete-one.js @@ -4,6 +4,10 @@ module.exports = { type: 'ref', required: true, }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -38,11 +42,12 @@ module.exports = { ); }); - await sails.helpers.utils.sendWebhook.with({ - event: 'PROJECT_DELETE', - data: project, - projectId: project.id, - user: inputs.request.currentUser, + sails.helpers.utils.sendWebhooks.with({ + event: 'projectDelete', + data: { + item: project, + }, + user: inputs.actorUser, }); } diff --git a/server/api/helpers/projects/update-one.js b/server/api/helpers/projects/update-one.js index 6b57a71c..f9e11eec 100644 --- a/server/api/helpers/projects/update-one.js +++ b/server/api/helpers/projects/update-one.js @@ -28,6 +28,10 @@ module.exports = { custom: valuesValidator, required: true, }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -109,11 +113,12 @@ module.exports = { ); }); - await sails.helpers.utils.sendWebhook.with({ - event: 'PROJECT_UPDATE', - data: project, - projectId: project.id, - user: inputs.request.currentUser, + sails.helpers.utils.sendWebhooks.with({ + event: 'projectUpdate', + data: { + item: project, + }, + user: inputs.actorUser, }); } diff --git a/server/api/helpers/tasks/create-one.js b/server/api/helpers/tasks/create-one.js index 7bb33182..5b156235 100644 --- a/server/api/helpers/tasks/create-one.js +++ b/server/api/helpers/tasks/create-one.js @@ -21,10 +21,22 @@ module.exports = { custom: valuesValidator, required: true, }, + project: { + type: 'ref', + required: true, + }, board: { type: 'ref', required: true, }, + list: { + type: 'ref', + required: true, + }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -54,6 +66,8 @@ module.exports = { position: nextPosition, }, }); + + // TODO: send webhooks }); const task = await Task.create({ @@ -71,13 +85,18 @@ module.exports = { inputs.request, ); - await sails.helpers.utils.sendWebhook.with({ - event: 'TASK_CREATE', - data: task, - projectId: inputs.board.projectId, - user: inputs.request.currentUser, - card: values.card, - board: inputs.board, + sails.helpers.utils.sendWebhooks.with({ + event: 'taskCreate', + data: { + item: task, + included: { + projects: [inputs.project], + boards: [inputs.board], + lists: [inputs.list], + cards: [values.card], + }, + }, + user: inputs.actorUser, }); return task; diff --git a/server/api/helpers/tasks/delete-one.js b/server/api/helpers/tasks/delete-one.js index 3b876aea..c413e39d 100644 --- a/server/api/helpers/tasks/delete-one.js +++ b/server/api/helpers/tasks/delete-one.js @@ -4,10 +4,26 @@ module.exports = { type: 'ref', required: true, }, + project: { + type: 'ref', + required: true, + }, board: { type: 'ref', required: true, }, + list: { + type: 'ref', + required: true, + }, + card: { + type: 'ref', + required: true, + }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -26,12 +42,18 @@ module.exports = { inputs.request, ); - await sails.helpers.utils.sendWebhook.with({ - event: 'TASK_DELETE', - data: task, - projectId: inputs.board.projectId, - user: inputs.request.currentUser, - board: inputs.board, + sails.helpers.utils.sendWebhooks.with({ + event: 'taskDelete', + data: { + item: task, + included: { + projects: [inputs.project], + boards: [inputs.board], + lists: [inputs.list], + cards: [inputs.card], + }, + }, + user: inputs.actorUser, }); } diff --git a/server/api/helpers/tasks/update-one.js b/server/api/helpers/tasks/update-one.js index 6d6b81ba..94c1d4b7 100644 --- a/server/api/helpers/tasks/update-one.js +++ b/server/api/helpers/tasks/update-one.js @@ -21,10 +21,26 @@ module.exports = { custom: valuesValidator, required: true, }, + project: { + type: 'ref', + required: true, + }, board: { type: 'ref', required: true, }, + list: { + type: 'ref', + required: true, + }, + card: { + type: 'ref', + required: true, + }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -57,6 +73,8 @@ module.exports = { position: nextPosition, }, }); + + // TODO: send webhooks }); } @@ -72,13 +90,18 @@ module.exports = { inputs.request, ); - await sails.helpers.utils.sendWebhook.with({ - event: 'TASK_UPDATE', - data: task, - projectId: inputs.board.projectId, - user: inputs.request.currentUser, - card: values.card, - board: inputs.board, + sails.helpers.utils.sendWebhooks.with({ + event: 'taskUpdate', + data: { + item: task, + included: { + projects: [inputs.project], + boards: [inputs.board], + lists: [inputs.list], + cards: [inputs.card], + }, + }, + user: inputs.actorUser, }); } diff --git a/server/api/helpers/users/create-one.js b/server/api/helpers/users/create-one.js index 87f36863..358ef679 100644 --- a/server/api/helpers/users/create-one.js +++ b/server/api/helpers/users/create-one.js @@ -27,6 +27,10 @@ module.exports = { custom: valuesValidator, required: true, }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -84,21 +88,12 @@ module.exports = { ); }); - /* The user could be created manually by an user or via OIDC. We hijack the id field, so one can differentiate between the two on the webhook side. */ - let initiator; - if (inputs.request && inputs.request.currentUser) { - initiator = inputs.request.currentUser; - } else { - initiator = { - id: 'oidc', - }; - } - - await sails.helpers.utils.sendWebhook.with({ - event: 'USER_CREATE', - data: { ...user, password: undefined }, - projectId: '', - user: initiator, + sails.helpers.utils.sendWebhooks.with({ + event: 'userCreate', + data: { + item: user, + }, + user: inputs.actorUser, }); return user; diff --git a/server/api/helpers/users/delete-one.js b/server/api/helpers/users/delete-one.js index 524db4b5..5c9c9f59 100644 --- a/server/api/helpers/users/delete-one.js +++ b/server/api/helpers/users/delete-one.js @@ -4,6 +4,10 @@ module.exports = { type: 'ref', required: true, }, + actorUser: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -60,11 +64,12 @@ module.exports = { ); }); - await sails.helpers.utils.sendWebhook.with({ - event: 'USER_DELETE', - data: { ...user, password: undefined }, - projectId: '', - user: inputs.request.currentUser, + sails.helpers.utils.sendWebhooks.with({ + event: 'userDelete', + data: { + item: user, + }, + user: inputs.actorUser, }); } diff --git a/server/api/helpers/users/get-or-create-one-using-oidc.js b/server/api/helpers/users/get-or-create-one-using-oidc.js index 2186c099..465a55b6 100644 --- a/server/api/helpers/users/get-or-create-one-using-oidc.js +++ b/server/api/helpers/users/get-or-create-one-using-oidc.js @@ -12,6 +12,7 @@ module.exports = { exits: { invalidCodeOrNonce: {}, + invalidUserinfoSignature: {}, missingValues: {}, emailAlreadyInUse: {}, usernameAlreadyInUse: {}, @@ -34,6 +35,14 @@ module.exports = { ); 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}`); throw 'invalidCodeOrNonce'; } @@ -88,8 +97,11 @@ module.exports = { // Otherwise, create a new user. if (!user) { - user = await sails.helpers.users - .createOne(values) + user = await sails.helpers.users.createOne + .with({ + values, + actorUser: User.OIDC, + }) .intercept('usernameAlreadyInUse', 'usernameAlreadyInUse'); } @@ -115,8 +127,12 @@ module.exports = { } if (Object.keys(updateValues).length > 0) { - user = await sails.helpers.users - .updateOne(user, updateValues, {}) // FIXME: hack for last parameter + user = await sails.helpers.users.updateOne + .with({ + record: user, + values: updateValues, + actorUser: User.OIDC, + }) .intercept('emailAlreadyInUse', 'emailAlreadyInUse') .intercept('usernameAlreadyInUse', 'usernameAlreadyInUse'); } diff --git a/server/api/helpers/users/update-one.js b/server/api/helpers/users/update-one.js index a829b43e..4d2b3910 100644 --- a/server/api/helpers/users/update-one.js +++ b/server/api/helpers/users/update-one.js @@ -38,7 +38,7 @@ module.exports = { custom: valuesValidator, required: true, }, - user: { + actorUser: { type: 'ref', required: true, }, @@ -62,14 +62,14 @@ module.exports = { let isOnlyPasswordChange = false; if (!_.isUndefined(values.password)) { + if (Object.keys(values).length === 1) { + isOnlyPasswordChange = true; + } + Object.assign(values, { password: bcrypt.hashSync(values.password, 10), passwordChangedAt: new Date().toISOString(), }); - - if (Object.keys(values).length === 1) { - isOnlyPasswordChange = true; - } } if (values.username) { @@ -118,7 +118,7 @@ module.exports = { inputs.request, ); - if (user.id === inputs.user.id && inputs.request && inputs.request.isSocket) { + if (user.id === inputs.actorUser.id && inputs.request && inputs.request.isSocket) { const tempRoom = uuid(); sails.sockets.addRoomMembersToRooms(`@user:${user.id}`, tempRoom, () => { @@ -153,14 +153,15 @@ module.exports = { inputs.request, ); }); - } - await sails.helpers.utils.sendWebhook.with({ - event: 'USER_UPDATE', - data: { ...user, password: undefined }, - projectId: '', - user: inputs.request.currentUser, - }); + sails.helpers.utils.sendWebhooks.with({ + event: 'userUpdate', + data: { + item: user, + }, + user: inputs.actorUser, + }); + } } return user; diff --git a/server/api/helpers/utils/jsonify-record.js b/server/api/helpers/utils/jsonify-record.js new file mode 100644 index 00000000..9927e09a --- /dev/null +++ b/server/api/helpers/utils/jsonify-record.js @@ -0,0 +1,14 @@ +module.exports = { + sync: true, + + inputs: { + record: { + type: 'ref', + required: true, + }, + }, + + fn(inputs) { + return inputs.record.toJSON ? inputs.record.toJSON() : inputs.record; + }, +}; diff --git a/server/api/helpers/utils/send-email.js b/server/api/helpers/utils/send-email.js index 02593f88..cb6a1e13 100644 --- a/server/api/helpers/utils/send-email.js +++ b/server/api/helpers/utils/send-email.js @@ -1,4 +1,6 @@ module.exports = { + // TODO: make sync? + inputs: { to: { type: 'string', diff --git a/server/api/helpers/utils/send-slack-message.js b/server/api/helpers/utils/send-slack-message.js index 510ae1b2..d17985e2 100644 --- a/server/api/helpers/utils/send-slack-message.js +++ b/server/api/helpers/utils/send-slack-message.js @@ -1,6 +1,8 @@ const POST_MESSAGE_API_URL = 'https://slack.com/api/chat.postMessage'; module.exports = { + // TODO: make sync? + inputs: { markdown: { type: 'string', diff --git a/server/api/helpers/utils/send-webhook.js b/server/api/helpers/utils/send-webhook.js deleted file mode 100644 index a33f7a89..00000000 --- a/server/api/helpers/utils/send-webhook.js +++ /dev/null @@ -1,115 +0,0 @@ -const EVENT_TYPES = { - ACTION_CREATE: 'action_create', - ACTION_UPDATE: 'action_update', - ACTION_DELETE: 'action_delete', - - CARD_CREATE: 'card_create', - CARD_UPDATE: 'card_update', - CARD_DELETE: 'card_delete', - - CARD_MEMBERSHIP_CREATE: 'card_membership_create', - CARD_MEMBERSHIP_DELETE: 'card_membership_delete', - - LIST_CREATE: 'list_create', - LIST_UPDATE: 'list_update', - LIST_DELETE: 'list_delete', - - BOARD_CREATE: 'board_create', - BOARD_UPDATE: 'board_update', - BOARD_DELETE: 'board_delete', - - ATTACHMENT_CREATE: 'attachment_create', - ATTACHMENT_UPDATE: 'attachment_update', - ATTACHMENT_DELETE: 'attachment_delete', - - PROJECT_CREATE: 'project_create', - PROJECT_UPDATE: 'project_update', - PROJECT_DELETE: 'project_delete', - - TASK_CREATE: 'task_create', - TASK_UPDATE: 'task_update', - TASK_DELETE: 'task_delete', - - USER_CREATE: 'user_create', - USER_UPDATE: 'user_update', - USER_DELETE: 'user_delete', -}; - -/** - * Sends a webhook notification to a configured URL. - * - * @param {Object} inputs - Data to include in the webhook payload. - * @param {string} inputs.event - The event type (see {@link EVENT_TYPES}). - * @param {*} inputs.data - The actual data related to the event. - * @param {string} inputs.projectId - The project ID associated with the event. - * @param {ref} [inputs.user] - Optional user object associated with the event. - * @param {ref} [inputs.card] - Optional card object associated with the event. - * @param {ref} [inputs.board] - Optional board object associated with the event. - * @param {ref} [inputs.list] - Optional list object associated with the event. - * @returns {Promise} - */ -async function sendWebhook(inputs) { - const url = sails.config.custom.webhookUrl; - const headers = { - 'Content-Type': 'application/json', - }; - if (sails.config.custom.webhookBearer) { - headers.Authorization = `Bearer ${sails.config.custom.webhookBearer}`; - } - - const body = JSON.stringify({ - ...inputs, - user: { - ...inputs.user, - password: undefined, - }, - }); - - const req = await fetch(url, { - method: 'POST', - headers, - body, - }); - if (req.status !== 200) { - sails.log.error(`Webhook failed with status ${req.status} and message: ${await req.text()}`); - } -} - -module.exports = { - eventTypes: EVENT_TYPES, - inputs: { - event: { - type: 'string', - isIn: Object.keys(EVENT_TYPES), - required: true, - }, - data: { - type: 'ref', - required: true, - }, - projectId: { - type: 'string', - default: '', - }, - user: { - type: 'ref', - }, - card: { - type: 'ref', - }, - board: { - type: 'ref', - }, - list: { - type: 'ref', - }, - }, - async fn(inputs) { - if (!sails.config.custom.webhookUrl) return; - try { - await sendWebhook(inputs); - } catch (err) { - sails.log.error(err); - } - }, -}; diff --git a/server/api/helpers/utils/send-webhooks.js b/server/api/helpers/utils/send-webhooks.js new file mode 100644 index 00000000..addfb8af --- /dev/null +++ b/server/api/helpers/utils/send-webhooks.js @@ -0,0 +1,178 @@ +const EVENT_TYPES = { + ACTION_CREATE: 'actionCreate', + ACTION_DELETE: 'actionDelete', + ACTION_UPDATE: 'actionUpdate', + + ATTACHMENT_CREATE: 'attachmentCreate', + ATTACHMENT_DELETE: 'attachmentDelete', + ATTACHMENT_UPDATE: 'attachmentUpdate', + + BOARD_CREATE: 'boardCreate', + BOARD_DELETE: 'boardDelete', + BOARD_UPDATE: 'boardUpdate', + + BOARD_MEMBERSHIP_CREATE: 'boardMembershipCreate', + BOARD_MEMBERSHIP_DELETE: 'boardMembershipDelete', + BOARD_MEMBERSHIP_UPDATE: 'boardMembershipUpdate', + + CARD_CREATE: 'cardCreate', + CARD_DELETE: 'cardDelete', + CARD_UPDATE: 'cardUpdate', + + CARD_LABEL_CREATE: 'cardLabelCreate', + CARD_LABEL_DELETE: 'cardLabelDelete', + + CARD_MEMBERSHIP_CREATE: 'cardMembershipCreate', + CARD_MEMBERSHIP_DELETE: 'cardMembershipDelete', + + LABEL_CREATE: 'labelCreate', + LABEL_DELETE: 'labelDelete', + LABEL_UPDATE: 'labelUpdate', + + LIST_CREATE: 'listCreate', + LIST_DELETE: 'listDelete', + LIST_SORT: 'listSort', + LIST_UPDATE: 'listUpdate', + + NOTIFICATION_CREATE: 'notificationCreate', + NOTIFICATION_UPDATE: 'notificationUpdate', + + PROJECT_CREATE: 'projectCreate', + PROJECT_DELETE: 'projectDelete', + PROJECT_UPDATE: 'projectUpdate', + + PROJECT_MANAGER_CREATE: 'projectManagerCreate', + PROJECT_MANAGER_DELETE: 'projectManagerDelete', + + TASK_CREATE: 'taskCreate', + TASK_DELETE: 'taskDelete', + TASK_UPDATE: 'taskUpdate', + + USER_CREATE: 'userCreate', + USER_DELETE: 'userDelete', + USER_UPDATE: 'userUpdate', +}; + +const jsonifyData = (data) => { + const nextData = {}; + + if (data.item) { + nextData.item = sails.helpers.utils.jsonifyRecord(data.item); + } + + if (data.items) { + nextData.items = data.items.map((item) => sails.helpers.utils.jsonifyRecord(item)); + } + + if (data.included) { + nextData.included = Object.entries(data.included).reduce( + (result, [key, items]) => ({ + ...result, + [key]: items.map((item) => sails.helpers.utils.jsonifyRecord(item)), + }), + {}, + ); + } + + return nextData; +}; + +/** + * @typedef {Object} Included + * @property {any[]} [projects] - Array of projects (optional). + * @property {any[]} [boards] - Array of boards (optional). + * @property {any[]} [lists] - Array of lists (optional). + * @property {any[]} [cards] - Array of cards (optional). + */ + +/** + * @typedef {Object} Data + * @property {any} item - Actual event data. + * @property {Included} [included] - Optional included data. + */ + +/** + * Sends a webhook notification to a configured URL. + * + * @param {*} webhook - Webhook configuration. + * @param {string} event - The event (see {@link EVENT_TYPES}). + * @param {Data} data - The data object containing event data and optionally included data. + * @param {ref} user - User object associated with the event. + * @returns {Promise} + */ +async function sendWebhook(webhook, event, data, user) { + const headers = { + 'Content-Type': 'application/json', + 'User-Agent': `planka (+${sails.config.custom.baseUrl})`, + }; + + if (webhook.accessToken) { + headers.Authorization = `Bearer ${webhook.accessToken}`; + } + + const body = JSON.stringify({ + event, + data: jsonifyData(data), + user: sails.helpers.utils.jsonifyRecord(user), + }); + + try { + const response = await fetch(webhook.url, { + headers, + body, + method: 'POST', + }); + + if (!response.ok) { + const message = await response.text(); + + sails.log.error( + `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}`); + } +} + +module.exports = { + sync: true, + + inputs: { + event: { + type: 'string', + required: true, + isIn: Object.values(EVENT_TYPES), + }, + data: { + type: 'ref', + required: true, + }, + user: { + type: 'ref', + required: true, + }, + }, + + fn(inputs) { + if (!sails.config.custom.webhooks) { + return; + } + + sails.config.custom.webhooks.forEach((webhook) => { + if (!webhook.url) { + return; + } + + if (webhook.excludedEvents && webhook.excludedEvents.includes(inputs.event)) { + return; + } + + if (webhook.events && !webhook.events.includes(inputs.event)) { + return; + } + + sendWebhook(webhook, inputs.event, inputs.data, inputs.user); + }); + }, +}; diff --git a/server/api/hooks/oidc/index.js b/server/api/hooks/oidc/index.js index 6dbeddd1..47c2bf7a 100644 --- a/server/api/hooks/oidc/index.js +++ b/server/api/hooks/oidc/index.js @@ -25,12 +25,19 @@ module.exports = function defineOidcHook(sails) { const issuer = await openidClient.Issuer.discover(sails.config.custom.oidcIssuer); - client = new issuer.Client({ + 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() { diff --git a/server/api/models/User.js b/server/api/models/User.js index 1d875318..486a66b2 100755 --- a/server/api/models/User.js +++ b/server/api/models/User.js @@ -5,7 +5,41 @@ * @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models */ +const LANGUAGES = [ + 'bg-BG', + 'cs-CZ', + 'da-DK', + 'de-DE', + 'en-US', + 'es-ES', + 'fa-IR', + 'fr-FR', + 'hu-HU', + 'id-ID', + 'it-IT', + 'ja-JP', + 'ko-KR', + 'nl-NL', + 'pl-PL', + 'pt-BR', + 'ro-RO', + 'ru-RU', + 'sk-SK', + 'sv-SE', + 'tr-TR', + 'uk-UA', + 'uz-UZ', + 'zh-CN', +]; + +const OIDC = { + id: '_oidc', +}; + module.exports = { + LANGUAGES, + OIDC, + attributes: { // ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗ // ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗ @@ -56,7 +90,7 @@ module.exports = { }, language: { type: 'string', - isNotEmptyString: true, + isIn: LANGUAGES, allowNull: true, }, subscribeToOwnCards: { diff --git a/server/config/custom.js b/server/config/custom.js index 8e335c50..6d641573 100644 --- a/server/config/custom.js +++ b/server/config/custom.js @@ -34,10 +34,16 @@ module.exports.custom = { defaultAdminEmail: process.env.DEFAULT_ADMIN_EMAIL && process.env.DEFAULT_ADMIN_EMAIL.toLowerCase(), + allowAllToCreateProjects: process.env.ALLOW_ALL_TO_CREATE_PROJECTS === 'true', + oidcIssuer: process.env.OIDC_ISSUER, oidcClientId: process.env.OIDC_CLIENT_ID, oidcClientSecret: process.env.OIDC_CLIENT_SECRET, + oidcIdTokenSignedResponseAlg: process.env.OIDC_ID_TOKEN_SIGNED_RESPONSE_ALG, + oidcUserinfoSignedResponseAlg: process.env.OIDC_USERINFO_SIGNED_RESPONSE_ALG, oidcScopes: process.env.OIDC_SCOPES || 'openid email profile', + 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(',') : [], oidcEmailAttribute: process.env.OIDC_EMAIL_ATTRIBUTE || 'email', oidcNameAttribute: process.env.OIDC_NAME_ATTRIBUTE || 'name', @@ -60,9 +66,8 @@ module.exports.custom = { smtpPassword: process.env.SMTP_PASSWORD, smtpFrom: process.env.SMTP_FROM, + webhooks: JSON.parse(process.env.WEBHOOKS || '[]'), // TODO: validate structure + slackBotToken: process.env.SLACK_BOT_TOKEN, slackChannelId: process.env.SLACK_CHANNEL_ID, - - webhookUrl: process.env.WEBHOOK_URL, - webhookBearer: process.env.WEBHOOK_BEARER, }; diff --git a/server/config/http.js b/server/config/http.js index c25ed29a..42d7a9cc 100644 --- a/server/config/http.js +++ b/server/config/http.js @@ -48,5 +48,7 @@ module.exports.http = { // var middlewareFn = skipper({ strict: true }); // return middlewareFn; // })(), + + poweredBy: false, }, }; diff --git a/server/config/policies.js b/server/config/policies.js index 665689df..a871368e 100644 --- a/server/config/policies.js +++ b/server/config/policies.js @@ -21,8 +21,6 @@ module.exports.policies = { 'users/create': ['is-authenticated', 'is-admin'], 'users/delete': ['is-authenticated', 'is-admin'], - 'projects/create': ['is-authenticated', 'is-admin'], - 'show-config': true, 'access-tokens/create': true, 'access-tokens/exchange-using-oidc': true, diff --git a/server/db/migrations/20220523131229_add_image_to_attachment_table.js b/server/db/migrations/20220523131229_add_image_to_attachment_table.js index 989f6957..25d6f62f 100644 --- a/server/db/migrations/20220523131229_add_image_to_attachment_table.js +++ b/server/db/migrations/20220523131229_add_image_to_attachment_table.js @@ -15,7 +15,7 @@ module.exports.up = async (knex) => { const attachments = await knex('attachment'); // eslint-disable-next-line no-restricted-syntax - for (attachment of attachments) { + for (const attachment of attachments) { if (attachment.is_image) { const image = sharp( path.join(config.custom.attachmentsPath, attachment.dirname, attachment.filename), @@ -54,7 +54,7 @@ module.exports.down = async (knex) => { const attachments = await knex('attachment'); // eslint-disable-next-line no-restricted-syntax - for (attachment of attachments) { + for (const attachment of attachments) { // eslint-disable-next-line no-await-in-loop await knex('attachment') .update({ diff --git a/server/db/migrations/20221223131625_preserve_original_format_of_images.js b/server/db/migrations/20221223131625_preserve_original_format_of_images.js index de2ec07c..f78762d2 100644 --- a/server/db/migrations/20221223131625_preserve_original_format_of_images.js +++ b/server/db/migrations/20221223131625_preserve_original_format_of_images.js @@ -93,7 +93,7 @@ module.exports.up = async (knex) => { const attachments = await knex('attachment').whereNotNull('image'); // eslint-disable-next-line no-restricted-syntax - for (attachment of attachments) { + for (const attachment of attachments) { // eslint-disable-next-line no-await-in-loop const image = await processAttachmentImage(attachment, config.custom.attachmentsPath); diff --git a/server/db/migrations/20221226210239_improve_quality_of_resized_images.js b/server/db/migrations/20221226210239_improve_quality_of_resized_images.js index fed01d40..b6e1e23c 100644 --- a/server/db/migrations/20221226210239_improve_quality_of_resized_images.js +++ b/server/db/migrations/20221226210239_improve_quality_of_resized_images.js @@ -113,7 +113,7 @@ module.exports.up = async (knex) => { const users = await knex('user_account').whereNotNull('avatar'); // eslint-disable-next-line no-restricted-syntax - for (user of users) { + for (const user of users) { // eslint-disable-next-line no-await-in-loop await processUserAvatar(user, config.custom.userAvatarsPath); } @@ -121,7 +121,7 @@ module.exports.up = async (knex) => { const projects = await knex('project').whereNotNull('background_image'); // eslint-disable-next-line no-restricted-syntax - for (project of projects) { + for (const project of projects) { // eslint-disable-next-line no-await-in-loop await processProjectBackgroundImage(project, config.custom.projectBackgroundImagesPath); } @@ -129,7 +129,7 @@ module.exports.up = async (knex) => { const attachments = await knex('attachment').whereNotNull('image'); // eslint-disable-next-line no-restricted-syntax - for (attachment of attachments) { + for (const attachment of attachments) { // eslint-disable-next-line no-await-in-loop await processAttachmentImage(attachment, config.custom.attachmentsPath); } diff --git a/server/db/migrations/20240721171239_languages_with_country_codes.js b/server/db/migrations/20240721171239_languages_with_country_codes.js new file mode 100644 index 00000000..083dae91 --- /dev/null +++ b/server/db/migrations/20240721171239_languages_with_country_codes.js @@ -0,0 +1,68 @@ +const _ = require('lodash'); + +const LANGUAGES = [ + 'bg-BG', + 'cs-CZ', + 'da-DK', + 'de-DE', + 'en-US', + 'es-ES', + 'fa-IR', + 'fr-FR', + 'hu-HU', + 'id-ID', + 'it-IT', + 'ja-JP', + 'ko-KR', + 'nl-NL', + 'pl-PL', + 'pt-BR', + 'ro-RO', + 'ru-RU', + 'sk-SK', + 'sv-SE', + 'tr-TR', + 'uz-UZ', + 'zh-CN', +]; + +const LANGUAGE_BY_PREV_LANGUAGE = LANGUAGES.reduce( + (result, language) => ({ + ...result, + [language.split('-')[0]]: language, + }), + {}, +); +LANGUAGE_BY_PREV_LANGUAGE.ua = 'uk-UA'; + +const PREV_LANGUAGE_BY_LANGUAGE = _.invert(LANGUAGE_BY_PREV_LANGUAGE); + +module.exports.up = async (knex) => { + const users = await knex('user_account').whereNotNull('language'); + const prevLanguages = [...new Set(users.map((user) => user.language))]; + + // eslint-disable-next-line no-restricted-syntax + for (const prevLanguage of prevLanguages) { + // eslint-disable-next-line no-await-in-loop + await knex('user_account') + .update({ + language: LANGUAGE_BY_PREV_LANGUAGE[prevLanguage], + }) + .where('language', prevLanguage); + } +}; + +module.exports.down = async (knex) => { + const users = await knex('user_account').whereNotNull('language'); + const languages = [...new Set(users.map((user) => user.language))]; + + // eslint-disable-next-line no-restricted-syntax + for (const language of languages) { + // eslint-disable-next-line no-await-in-loop + await knex('user_account') + .update({ + language: PREV_LANGUAGE_BY_LANGUAGE[language], + }) + .where('language', language); + } +}; diff --git a/server/package-lock.json b/server/package-lock.json index 3551a014..6487f43c 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -2164,9 +2164,9 @@ } }, "node_modules/engine.io": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", - "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", + "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -2177,7 +2177,7 @@ "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", - "ws": "~8.11.0" + "ws": "~8.17.1" }, "engines": { "node": ">=10.2.0" @@ -7119,12 +7119,12 @@ } }, "node_modules/socket.io-adapter": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", - "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", "dependencies": { "debug": "~4.3.4", - "ws": "~8.11.0" + "ws": "~8.17.1" } }, "node_modules/socket.io-parser": { @@ -8190,15 +8190,15 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": {