Merge branch 'plankanban:master' into master

pull/237/head
Aliaksandr Shulyak 4 years ago committed by GitHub
commit 50b7d6fc93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,42 @@
name: Build and push Docker image
on:
release:
types: [created]
jobs:
build-and-push-docker-image:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set version
uses: actions/github-script@v6
id: set-version
with:
result-encoding: string
script: return context.payload.release.tag_name.replace('v', '')
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: |
ghcr.io/plankanban/planka:latest
ghcr.io/plankanban/planka:${{ steps.set-version.outputs.result }}

@ -47,15 +47,17 @@ RUN apk -U upgrade \
&& apk del vips-dependencies --purge && apk del vips-dependencies --purge
COPY docker-start.sh start.sh COPY docker-start.sh start.sh
RUN chmod +x start.sh
COPY server . COPY server .
RUN chmod +x start.sh \
&& cp .env.sample .env
COPY --from=client-builder /app/build public COPY --from=client-builder /app/build public
COPY --from=client-builder /app/build/index.html views COPY --from=client-builder /app/build/index.html views
VOLUME /app/public/user-avatars VOLUME /app/public/user-avatars
VOLUME /app/public/project-background-images VOLUME /app/public/project-background-images
VOLUME /app/public/attachments VOLUME /app/private/attachments
EXPOSE 1337 EXPOSE 1337

@ -61,7 +61,7 @@ Either use a local database or start the provided development database:
docker-compose -f docker-compose-dev.yml up docker-compose -f docker-compose-dev.yml up
``` ```
Edit `DATABASE_URL` in `.env` file if needed, then initialize the database: Create `server/.env` based on `server/.env.sample` and edit `DATABASE_URL` if needed, then initialize the database:
``` ```
npm run server:db:init npm run server:db:init

@ -14,6 +14,7 @@
"i18next": "^21.6.16", "i18next": "^21.6.16",
"i18next-browser-languagedetector": "^6.1.4", "i18next-browser-languagedetector": "^6.1.4",
"initials": "^3.1.2", "initials": "^3.1.2",
"js-cookie": "^3.0.1",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"node-sass": "^7.0.1", "node-sass": "^7.0.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
@ -13534,6 +13535,14 @@
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz",
"integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==" "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ=="
}, },
"node_modules/js-cookie": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz",
"integrity": "sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==",
"engines": {
"node": ">=12"
}
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -33059,6 +33068,11 @@
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz",
"integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==" "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ=="
}, },
"js-cookie": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz",
"integrity": "sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw=="
},
"js-tokens": { "js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",

@ -71,6 +71,7 @@
"i18next": "^21.6.16", "i18next": "^21.6.16",
"i18next-browser-languagedetector": "^6.1.4", "i18next-browser-languagedetector": "^6.1.4",
"initials": "^3.1.2", "initials": "^3.1.2",
"js-cookie": "^3.0.1",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"node-sass": "^7.0.1", "node-sass": "^7.0.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",

@ -2,7 +2,7 @@ import http from './http';
/* Actions */ /* Actions */
const createAccessToken = (data, headers) => http.post('/access-tokens', data, headers); const createAccessToken = (data) => http.post('/access-tokens', data);
export default { export default {
createAccessToken, createAccessToken,

@ -9,8 +9,8 @@ export const transformAction = (action) => ({
/* Actions */ /* Actions */
const getActions = (cardId, data, headers) => const getActions = (cardId, data) =>
socket.get(`/cards/${cardId}/actions`, data, headers).then((body) => ({ socket.get(`/cards/${cardId}/actions`, data).then((body) => ({
...body, ...body,
items: body.items.map(transformAction), items: body.items.map(transformAction),
})); }));

@ -10,20 +10,20 @@ export const transformAttachment = (attachment) => ({
/* Actions */ /* Actions */
const createAttachment = (cardId, data, requestId, headers) => const createAttachment = (cardId, data, requestId) =>
http.post(`/cards/${cardId}/attachments?requestId=${requestId}`, data, headers).then((body) => ({ http.post(`/cards/${cardId}/attachments?requestId=${requestId}`, data).then((body) => ({
...body, ...body,
item: transformAttachment(body.item), item: transformAttachment(body.item),
})); }));
const updateAttachment = (id, data, headers) => const updateAttachment = (id, data) =>
socket.patch(`/attachments/${id}`, data, headers).then((body) => ({ socket.patch(`/attachments/${id}`, data).then((body) => ({
...body, ...body,
item: transformAttachment(body.item), item: transformAttachment(body.item),
})); }));
const deleteAttachment = (id, headers) => const deleteAttachment = (id) =>
socket.delete(`/attachments/${id}`, undefined, headers).then((body) => ({ socket.delete(`/attachments/${id}`).then((body) => ({
...body, ...body,
item: transformAttachment(body.item), item: transformAttachment(body.item),
})); }));

@ -2,11 +2,10 @@ import socket from './socket';
/* Actions */ /* Actions */
const createBoardMembership = (boardId, data, headers) => const createBoardMembership = (boardId, data) =>
socket.post(`/boards/${boardId}/memberships`, data, headers); socket.post(`/boards/${boardId}/memberships`, data);
const deleteBoardMembership = (id, headers) => const deleteBoardMembership = (id) => socket.delete(`/board-memberships/${id}`);
socket.delete(`/board-memberships/${id}`, undefined, headers);
export default { export default {
createBoardMembership, createBoardMembership,

@ -4,11 +4,10 @@ import { transformAttachment } from './attachments';
/* Actions */ /* Actions */
const createBoard = (projectId, data, headers) => const createBoard = (projectId, data) => socket.post(`/projects/${projectId}/boards`, data);
socket.post(`/projects/${projectId}/boards`, data, headers);
const getBoard = (id, headers) => const getBoard = (id) =>
socket.get(`/boards/${id}`, undefined, headers).then((body) => ({ socket.get(`/boards/${id}`).then((body) => ({
...body, ...body,
included: { included: {
...body.included, ...body.included,
@ -17,9 +16,9 @@ const getBoard = (id, headers) =>
}, },
})); }));
const updateBoard = (id, data, headers) => socket.patch(`/boards/${id}`, data, headers); const updateBoard = (id, data) => socket.patch(`/boards/${id}`, data);
const deleteBoard = (id, headers) => socket.delete(`/boards/${id}`, undefined, headers); const deleteBoard = (id) => socket.delete(`/boards/${id}`);
export default { export default {
createBoard, createBoard,

@ -2,11 +2,9 @@ import socket from './socket';
/* Actions */ /* Actions */
const createCardLabel = (cardId, data, headers) => const createCardLabel = (cardId, data) => socket.post(`/cards/${cardId}/labels`, data);
socket.post(`/cards/${cardId}/labels`, data, headers);
const deleteCardLabel = (cardId, labelId, headers) => const deleteCardLabel = (cardId, labelId) => socket.delete(`/cards/${cardId}/labels/${labelId}`);
socket.delete(`/cards/${cardId}/labels/${labelId}`, undefined, headers);
export default { export default {
createCardLabel, createCardLabel,

@ -2,11 +2,10 @@ import socket from './socket';
/* Actions */ /* Actions */
const createCardMembership = (cardId, data, headers) => const createCardMembership = (cardId, data) => socket.post(`/cards/${cardId}/memberships`, data);
socket.post(`/cards/${cardId}/memberships`, data, headers);
const deleteCardMembership = (cardId, userId, headers) => const deleteCardMembership = (cardId, userId) =>
socket.delete(`/cards/${cardId}/memberships?userId=${userId}`, undefined, headers); socket.delete(`/cards/${cardId}/memberships?userId=${userId}`);
export default { export default {
createCardMembership, createCardMembership,

@ -35,8 +35,8 @@ export const transformCardData = (data) => ({
/* Actions */ /* Actions */
const getCards = (boardId, data, headers) => const getCards = (boardId, data) =>
socket.get(`/board/${boardId}/cards`, data, headers).then((body) => ({ socket.get(`/board/${boardId}/cards`, data).then((body) => ({
...body, ...body,
items: body.items.map(transformCard), items: body.items.map(transformCard),
included: { included: {
@ -45,26 +45,26 @@ const getCards = (boardId, data, headers) =>
}, },
})); }));
const createCard = (boardId, data, headers) => const createCard = (boardId, data) =>
socket.post(`/boards/${boardId}/cards`, transformCardData(data), headers).then((body) => ({ socket.post(`/boards/${boardId}/cards`, transformCardData(data)).then((body) => ({
...body, ...body,
item: transformCard(body.item), item: transformCard(body.item),
})); }));
const getCard = (id, headers) => const getCard = (id) =>
socket.get(`/cards/${id}`, undefined, headers).then((body) => ({ socket.get(`/cards/${id}`).then((body) => ({
...body, ...body,
item: transformCard(body.item), item: transformCard(body.item),
})); }));
const updateCard = (id, data, headers) => const updateCard = (id, data) =>
socket.patch(`/cards/${id}`, transformCardData(data), headers).then((body) => ({ socket.patch(`/cards/${id}`, transformCardData(data)).then((body) => ({
...body, ...body,
item: transformCard(body.item), item: transformCard(body.item),
})); }));
const deleteCard = (id, headers) => const deleteCard = (id) =>
socket.delete(`/cards/${id}`, undefined, headers).then((body) => ({ socket.delete(`/cards/${id}`).then((body) => ({
...body, ...body,
item: transformCard(body.item), item: transformCard(body.item),
})); }));

@ -3,20 +3,20 @@ import { transformAction } from './actions';
/* Actions */ /* Actions */
const createCommentAction = (cardId, data, headers) => const createCommentAction = (cardId, data) =>
socket.post(`/cards/${cardId}/comment-actions`, data, headers).then((body) => ({ socket.post(`/cards/${cardId}/comment-actions`, data).then((body) => ({
...body, ...body,
item: transformAction(body.item), item: transformAction(body.item),
})); }));
const updateCommentAction = (id, data, headers) => const updateCommentAction = (id, data) =>
socket.patch(`/comment-actions/${id}`, data, headers).then((body) => ({ socket.patch(`/comment-actions/${id}`, data).then((body) => ({
...body, ...body,
item: transformAction(body.item), item: transformAction(body.item),
})); }));
const deleteCommentAction = (id, headers) => const deleteCommentAction = (id) =>
socket.delete(`/comment-actions/${id}`, undefined, headers).then((body) => ({ socket.delete(`/comment-actions/${id}`).then((body) => ({
...body, ...body,
item: transformAction(body.item), item: transformAction(body.item),
})); }));

@ -6,7 +6,7 @@ const http = {};
// TODO: add all methods // TODO: add all methods
['POST'].forEach((method) => { ['POST'].forEach((method) => {
http[method.toLowerCase()] = (url, data, headers) => { http[method.toLowerCase()] = (url, data) => {
const formData = Object.keys(data).reduce((result, key) => { const formData = Object.keys(data).reduce((result, key) => {
result.append(key, data[key]); result.append(key, data[key]);
@ -15,8 +15,8 @@ const http = {};
return fetch(`${Config.SERVER_BASE_URL}/api${url}`, { return fetch(`${Config.SERVER_BASE_URL}/api${url}`, {
method, method,
headers,
body: formData, body: formData,
...Config.FETCH_OPTIONS,
}) })
.then((response) => .then((response) =>
response.json().then((body) => ({ response.json().then((body) => ({

@ -2,12 +2,11 @@ import socket from './socket';
/* Actions */ /* Actions */
const createLabel = (boardId, data, headers) => const createLabel = (boardId, data) => socket.post(`/boards/${boardId}/labels`, data);
socket.post(`/boards/${boardId}/labels`, data, headers);
const updateLabel = (id, data, headers) => socket.patch(`/labels/${id}`, data, headers); const updateLabel = (id, data) => socket.patch(`/labels/${id}`, data);
const deleteLabel = (id, headers) => socket.delete(`/labels/${id}`, undefined, headers); const deleteLabel = (id) => socket.delete(`/labels/${id}`);
export default { export default {
createLabel, createLabel,

@ -2,12 +2,11 @@ import socket from './socket';
/* Actions */ /* Actions */
const createList = (boardId, data, headers) => const createList = (boardId, data) => socket.post(`/boards/${boardId}/lists`, data);
socket.post(`/boards/${boardId}/lists`, data, headers);
const updateList = (id, data, headers) => socket.patch(`/lists/${id}`, data, headers); const updateList = (id, data) => socket.patch(`/lists/${id}`, data);
const deleteList = (id, headers) => socket.delete(`/lists/${id}`, undefined, headers); const deleteList = (id) => socket.delete(`/lists/${id}`);
export default { export default {
createList, createList,

@ -4,8 +4,8 @@ import { transformAction } from './actions';
/* Actions */ /* Actions */
const getNotifications = (headers) => const getNotifications = () =>
socket.get('/notifications', undefined, headers).then((body) => ({ socket.get('/notifications').then((body) => ({
...body, ...body,
included: { included: {
...body.included, ...body.included,
@ -14,8 +14,8 @@ const getNotifications = (headers) =>
}, },
})); }));
const getNotification = (id, headers) => const getNotification = (id) =>
socket.get(`/notifications/${id}`, undefined, headers).then((body) => ({ socket.get(`/notifications/${id}`).then((body) => ({
...body, ...body,
included: { included: {
...body.included, ...body.included,
@ -24,8 +24,7 @@ const getNotification = (id, headers) =>
}, },
})); }));
const updateNotifications = (ids, data, headers) => const updateNotifications = (ids, data) => socket.patch(`/notifications/${ids.join(',')}`, data);
socket.patch(`/notifications/${ids.join(',')}`, data, headers);
export default { export default {
getNotifications, getNotifications,

@ -2,11 +2,10 @@ import socket from './socket';
/* Actions */ /* Actions */
const createProjectManager = (projectId, data, headers) => const createProjectManager = (projectId, data) =>
socket.post(`/projects/${projectId}/managers`, data, headers); socket.post(`/projects/${projectId}/managers`, data);
const deleteProjectManager = (id, headers) => const deleteProjectManager = (id) => socket.delete(`/project-managers/${id}`);
socket.delete(`/project-managers/${id}`, undefined, headers);
export default { export default {
createProjectManager, createProjectManager,

@ -3,18 +3,18 @@ import socket from './socket';
/* Actions */ /* Actions */
const getProjects = (headers) => socket.get('/projects', undefined, headers); const getProjects = () => socket.get('/projects');
const createProject = (data, headers) => socket.post('/projects', data, headers); const createProject = (data) => socket.post('/projects', data);
const getProject = (id, headers) => socket.get(`/projects/${id}`, undefined, headers); const getProject = (id) => socket.get(`/projects/${id}`);
const updateProject = (id, data, headers) => socket.patch(`/projects/${id}`, data, headers); const updateProject = (id, data) => socket.patch(`/projects/${id}`, data);
const updateProjectBackgroundImage = (id, data, headers) => const updateProjectBackgroundImage = (id, data) =>
http.post(`/projects/${id}/background-image`, data, headers); http.post(`/projects/${id}/background-image`, data);
const deleteProject = (id, headers) => socket.delete(`/projects/${id}`, undefined, headers); const deleteProject = (id) => socket.delete(`/projects/${id}`);
export default { export default {
getProjects, getProjects,

@ -16,13 +16,12 @@ const { socket } = io;
socket.connect = socket._connect; // eslint-disable-line no-underscore-dangle socket.connect = socket._connect; // eslint-disable-line no-underscore-dangle
['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].forEach((method) => { ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].forEach((method) => {
socket[method.toLowerCase()] = (url, data, headers) => socket[method.toLowerCase()] = (url, data) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
socket.request( socket.request(
{ {
method, method,
data, data,
headers,
url: `/api${url}`, url: `/api${url}`,
}, },
(_, { body, error }) => { (_, { body, error }) => {

@ -2,11 +2,11 @@ import socket from './socket';
/* Actions */ /* Actions */
const createTask = (cardId, data, headers) => socket.post(`/cards/${cardId}/tasks`, data, headers); const createTask = (cardId, data) => socket.post(`/cards/${cardId}/tasks`, data);
const updateTask = (id, data, headers) => socket.patch(`/tasks/${id}`, data, headers); const updateTask = (id, data) => socket.patch(`/tasks/${id}`, data);
const deleteTask = (id, headers) => socket.delete(`/tasks/${id}`, undefined, headers); const deleteTask = (id) => socket.delete(`/tasks/${id}`);
export default { export default {
createTask, createTask,

@ -3,27 +3,25 @@ import socket from './socket';
/* Actions */ /* Actions */
const getUsers = (headers) => socket.get('/users', undefined, headers); const getUsers = () => socket.get('/users');
const createUser = (data, headers) => socket.post('/users', data, headers); const createUser = (data) => socket.post('/users', data);
const getUser = (id, headers) => socket.get(`/users/${id}`, undefined, headers); const getUser = (id) => socket.get(`/users/${id}`);
const getCurrentUser = (headers) => socket.get('/users/me', undefined, headers); const getCurrentUser = () => socket.get('/users/me');
const updateUser = (id, data, headers) => socket.patch(`/users/${id}`, data, headers); const updateUser = (id, data) => socket.patch(`/users/${id}`, data);
const updateUserEmail = (id, data, headers) => socket.patch(`/users/${id}/email`, data, headers); const updateUserEmail = (id, data) => socket.patch(`/users/${id}/email`, data);
const updateUserPassword = (id, data, headers) => const updateUserPassword = (id, data) => socket.patch(`/users/${id}/password`, data);
socket.patch(`/users/${id}/password`, data, headers);
const updateUserUsername = (id, data, headers) => const updateUserUsername = (id, data) => socket.patch(`/users/${id}/username`, data);
socket.patch(`/users/${id}/username`, data, headers);
const updateUserAvatar = (id, data, headers) => http.post(`/users/${id}/avatar`, data, headers); const updateUserAvatar = (id, data) => http.post(`/users/${id}/avatar`, data);
const deleteUser = (id, headers) => socket.delete(`/users/${id}`, undefined, headers); const deleteUser = (id) => socket.delete(`/users/${id}`);
export default { export default {
getUsers, getUsers,

@ -2,12 +2,24 @@ const SERVER_BASE_URL =
process.env.REACT_APP_SERVER_BASE_URL || process.env.REACT_APP_SERVER_BASE_URL ||
(process.env.NODE_ENV === 'production' ? '' : 'http://localhost:1337'); (process.env.NODE_ENV === 'production' ? '' : 'http://localhost:1337');
const POSITION_GAP = 65535; const FETCH_OPTIONS =
process.env.NODE_ENV === 'production'
? undefined
: {
credentials: 'include',
};
const ACCESS_TOKEN_KEY = 'accessToken';
const ACCESS_TOKEN_EXPIRES = 365;
const POSITION_GAP = 65535;
const ACTIONS_LIMIT = 10; const ACTIONS_LIMIT = 10;
export default { export default {
SERVER_BASE_URL, SERVER_BASE_URL,
FETCH_OPTIONS,
ACCESS_TOKEN_KEY,
ACCESS_TOKEN_EXPIRES,
POSITION_GAP, POSITION_GAP,
ACTIONS_LIMIT, ACTIONS_LIMIT,
}; };

@ -17,6 +17,7 @@ export default {
actions: 'Aktionen', actions: 'Aktionen',
addAttachment_title: 'Anhang hinzufügen', addAttachment_title: 'Anhang hinzufügen',
addComment: 'Kommentar hinzufügen', addComment: 'Kommentar hinzufügen',
addManager_title: 'Manager hinzufügen',
addMember_title: 'Mitglied hinzufügen', addMember_title: 'Mitglied hinzufügen',
addUser_title: 'Benutzer hinzufügen', addUser_title: 'Benutzer hinzufügen',
administrator: 'Administrator', administrator: 'Administrator',
@ -36,11 +37,16 @@ export default {
areYouSureYouWantToDeleteThisTask: 'Sind Sie sicher, dass Sie diese Aufgabe löschen möchten?', areYouSureYouWantToDeleteThisTask: 'Sind Sie sicher, dass Sie diese Aufgabe löschen möchten?',
areYouSureYouWantToDeleteThisUser: areYouSureYouWantToDeleteThisUser:
'Sind Sie sicher, dass Sie diesen Benutzer löschen möchten?', 'Sind Sie sicher, dass Sie diesen Benutzer löschen möchten?',
areYouSureYouWantToRemoveThisMemberFromProject: areYouSureYouWantToLeaveBoard: 'Sind Sie sicher, dass Sie das Board verlassen möchten?',
'Sind Sie sicher, dass Sie dieses Mitglied aus dem Projekt entfernen möchten??', areYouSureYouWantToLeaveProject: 'Sind Sie sicher, dass Sie das Projekt verlassen möchten?',
areYouSureYouWantToRemoveThisManagerFromProject:
'Sind Sie sicher, dass Sie diesen Manager aus dem Projekt entfernen möchten?',
areYouSureYouWantToRemoveThisMemberFromBoard:
'Sind Sie sicher, dass Sie dieses Mitglied aus dem Board entfernen möchten?',
attachment: 'Anhang', attachment: 'Anhang',
attachments: 'Anhänge', attachments: 'Anhänge',
authentication: 'Authentifizierung', authentication: 'Authentifizierung',
background: 'Hintergrund',
board: 'Board', board: 'Board',
boardNotFound_title: 'Board nicht gefunden', boardNotFound_title: 'Board nicht gefunden',
cardActions_title: 'Kartenaktionen', cardActions_title: 'Kartenaktionen',
@ -53,6 +59,7 @@ export default {
createProject_title: 'Projekt erstellen', createProject_title: 'Projekt erstellen',
createTextFile_title: 'Textdatei erstellen', createTextFile_title: 'Textdatei erstellen',
currentPassword: 'Derzeitiges Password', currentPassword: 'Derzeitiges Password',
dangerZone_title: 'Gefährlicher Bereich',
date: 'Datum', date: 'Datum',
dueDate_title: 'Fälligkeitsdatum', dueDate_title: 'Fälligkeitsdatum',
deleteAttachment_title: 'Anhang löschen', deleteAttachment_title: 'Anhang löschen',
@ -86,11 +93,15 @@ export default {
filterByLabels_title: 'Nach Label filtern', filterByLabels_title: 'Nach Label filtern',
filterByMembers_title: 'Nach Mitgliedern filtern', filterByMembers_title: 'Nach Mitgliedern filtern',
fromComputer_title: 'Vom Computer', fromComputer_title: 'Vom Computer',
general: 'Allgemein',
hours: 'Stunden', hours: 'Stunden',
invalidCurrentPassword: 'Das aktuelle Passwort ist falsch', invalidCurrentPassword: 'Das aktuelle Passwort ist falsch',
labels: 'Labels', labels: 'Labels',
leaveBoard_title: 'Board verlassen',
leaveProject_title: 'Projekt verlassen',
list: 'Listen', list: 'Listen',
listActions_title: 'Aufgaben auflisten', listActions_title: 'Aufgaben auflisten',
managers: 'Manager',
members: 'Mitglieder', members: 'Mitglieder',
minutes: 'Minuten', minutes: 'Minuten',
moveCard_title: 'Karte verschieben', moveCard_title: 'Karte verschieben',
@ -113,6 +124,7 @@ export default {
'Tipp: Drücken Sie STRG-V (Cmd-V auf Mac), um einen Anhang aus der Zwischenablage hinzuzufügen.', 'Tipp: Drücken Sie STRG-V (Cmd-V auf Mac), um einen Anhang aus der Zwischenablage hinzuzufügen.',
project: 'Projekt', project: 'Projekt',
projectNotFound_title: 'Projekt nicht gefunden', projectNotFound_title: 'Projekt nicht gefunden',
removeManager_title: 'Manager entfernen',
removeMember_title: 'Mitglied entfernen', removeMember_title: 'Mitglied entfernen',
seconds: 'Sekunden', seconds: 'Sekunden',
selectBoard: 'Board auswählen', selectBoard: 'Board auswählen',
@ -180,6 +192,8 @@ export default {
editTimer_title: 'Timer bearbeiten', editTimer_title: 'Timer bearbeiten',
editTitle_title: 'Titel bearbeiten', editTitle_title: 'Titel bearbeiten',
editUsername_title: 'Benutzername ändern', editUsername_title: 'Benutzername ändern',
leaveBoard: 'Board verlassen',
leaveProject: 'Projekt verlassen',
logOut_title: 'Ausloggen', logOut_title: 'Ausloggen',
makeCover_title: 'als Vorschau festlegen', makeCover_title: 'als Vorschau festlegen',
move: 'Verschieben', move: 'Verschieben',
@ -187,7 +201,9 @@ export default {
remove: 'Löschen', remove: 'Löschen',
removeBackground: 'Hintergrund löschen', removeBackground: 'Hintergrund löschen',
removeCover_title: 'Vorschau löschen', removeCover_title: 'Vorschau löschen',
removeFromBoard: 'Vom Board entfernen',
removeFromProject: 'Vom Projekt entfernen', removeFromProject: 'Vom Projekt entfernen',
removeManager: 'Manager entfernen',
removeMember: 'Mitglied entfernen', removeMember: 'Mitglied entfernen',
save: 'Speichern', save: 'Speichern',
showAllAttachments: 'Alle Anhänge anzeigen ({{hidden}} versteckt)', showAllAttachments: 'Alle Anhänge anzeigen ({{hidden}} versteckt)',

@ -1,19 +1,12 @@
import { getAccessToken } from '../utils/access-token-storage';
import ActionTypes from '../constants/ActionTypes'; import ActionTypes from '../constants/ActionTypes';
const initialState = { const initialState = {
accessToken: getAccessToken(),
userId: null, userId: null,
}; };
// eslint-disable-next-line default-param-last // eslint-disable-next-line default-param-last
export default (state = initialState, { type, payload }) => { export default (state = initialState, { type, payload }) => {
switch (type) { switch (type) {
case ActionTypes.AUTHENTICATE__SUCCESS:
return {
...state,
accessToken: payload.accessToken,
};
case ActionTypes.SOCKET_RECONNECT_HANDLE: case ActionTypes.SOCKET_RECONNECT_HANDLE:
case ActionTypes.CORE_INITIALIZE: case ActionTypes.CORE_INITIALIZE:
return { return {

@ -1,6 +1,5 @@
import { call, fork, join, put, select, take } from 'redux-saga/effects'; import { call, fork, join, put, take } from 'redux-saga/effects';
import { accessTokenSelector } from '../../selectors';
import { logout } from '../../actions'; import { logout } from '../../actions';
import ErrorCodes from '../../constants/ErrorCodes'; import ErrorCodes from '../../constants/ErrorCodes';
@ -13,12 +12,8 @@ function* queueRequest(method, ...args) {
} catch {} // eslint-disable-line no-empty } catch {} // eslint-disable-line no-empty
} }
const accessToken = yield select(accessTokenSelector);
try { try {
return yield call(method, ...args, { return yield call(method, ...args);
Authorization: `Bearer ${accessToken}`,
});
} catch (error) { } catch (error) {
if (error.code === ErrorCodes.UNAUTHORIZED) { if (error.code === ErrorCodes.UNAUTHORIZED) {
yield put(logout()); // TODO: next url yield put(logout()); // TODO: next url

@ -1,11 +1,11 @@
import { call, select } from 'redux-saga/effects'; import { call } from 'redux-saga/effects';
import loginSaga from './login'; import loginSaga from './login';
import coreSaga from './core'; import coreSaga from './core';
import { accessTokenSelector } from '../selectors'; import { getAccessToken } from '../utils/access-token-storage';
export default function* rootSaga() { export default function* rootSaga() {
const accessToken = yield select(accessTokenSelector); const accessToken = yield call(getAccessToken);
if (!accessToken) { if (!accessToken) {
yield call(loginSaga); yield call(loginSaga);

@ -1,11 +1,26 @@
const ACCESS_TOKEN_KEY = 'accessToken'; import Cookies from 'js-cookie';
export const getAccessToken = () => localStorage.getItem(ACCESS_TOKEN_KEY); import Config from '../constants/Config';
export const setAccessToken = (accessToken) => { export const setAccessToken = (accessToken) => {
localStorage.setItem(ACCESS_TOKEN_KEY, accessToken); Cookies.set(Config.ACCESS_TOKEN_KEY, accessToken, {
expires: Config.ACCESS_TOKEN_EXPIRES,
});
};
export const getAccessToken = () => {
// TODO: remove migration
const accessToken = localStorage.getItem(Config.ACCESS_TOKEN_KEY);
if (accessToken) {
localStorage.removeItem(Config.ACCESS_TOKEN_KEY);
setAccessToken(accessToken);
return accessToken;
}
return Cookies.get(Config.ACCESS_TOKEN_KEY);
}; };
export const removeAccessToken = () => { export const removeAccessToken = () => {
localStorage.removeItem(ACCESS_TOKEN_KEY); Cookies.remove(Config.ACCESS_TOKEN_KEY);
}; };

@ -15,7 +15,7 @@ services:
volumes: volumes:
- user-avatars:/app/public/user-avatars - user-avatars:/app/public/user-avatars
- project-background-images:/app/public/project-background-images - project-background-images:/app/public/project-background-images
- attachments:/app/public/attachments - attachments:/app/private/attachments
ports: ports:
- 3000:1337 - 3000:1337
environment: environment:

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

9
server/.gitignore vendored

@ -44,6 +44,7 @@
# #
################################################ ################################################
.env
config/local.js config/local.js
################################################ ################################################
@ -134,9 +135,11 @@ public/user-avatars/*
!public/project-background-images !public/project-background-images
public/project-background-images/* public/project-background-images/*
!public/project-background-images/.gitkeep !public/project-background-images/.gitkeep
!public/attachments
public/attachments/* private/*
!public/attachments/.gitkeep !private/attachments
private/attachments/*
!private/attachments/.gitkeep
views/* views/*
!views/.gitkeep !views/.gitkeep

@ -0,0 +1,69 @@
const fs = require('fs');
const path = require('path');
const Errors = {
ATTACHMENT_NOT_FOUND: {
attachmentNotFound: 'Attachment not found',
},
};
module.exports = {
inputs: {
id: {
type: 'string',
regex: /^[0-9]+$/,
required: true,
},
filename: {
type: 'string',
required: true,
},
},
exits: {
attachmentNotFound: {
responseType: 'notFound',
},
},
async fn(inputs, exits) {
const { currentUser } = this.req;
const { attachment, card, project } = await sails.helpers.attachments
.getProjectPath(inputs.id)
.intercept('pathNotFound', () => Errors.ATTACHMENT_NOT_FOUND);
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, card.boardId);
if (!isBoardMember) {
const isProjectManager = await sails.helpers.users.isProjectManager(
currentUser.id,
project.id,
);
if (!isProjectManager) {
throw Errors.ATTACHMENT_NOT_FOUND; // Forbidden
}
}
if (!attachment.isImage) {
throw Errors.ATTACHMENT_NOT_FOUND;
}
const filePath = path.join(
sails.config.custom.attachmentsPath,
attachment.dirname,
'thumbnails',
inputs.filename,
);
if (!fs.existsSync(filePath)) {
throw Errors.ATTACHMENT_NOT_FOUND;
}
this.res.type(attachment.filename);
this.res.set('Cache-Control', 'private, max-age=900'); // TODO: move to config
return exits.success(fs.createReadStream(filePath));
},
};

@ -0,0 +1,63 @@
const fs = require('fs');
const path = require('path');
const Errors = {
ATTACHMENT_NOT_FOUND: {
attachmentNotFound: 'Attachment not found',
},
};
module.exports = {
inputs: {
id: {
type: 'string',
regex: /^[0-9]+$/,
required: true,
},
},
exits: {
attachmentNotFound: {
responseType: 'notFound',
},
},
async fn(inputs, exits) {
const { currentUser } = this.req;
const { attachment, card, project } = await sails.helpers.attachments
.getProjectPath(inputs.id)
.intercept('pathNotFound', () => Errors.ATTACHMENT_NOT_FOUND);
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, card.boardId);
if (!isBoardMember) {
const isProjectManager = await sails.helpers.users.isProjectManager(
currentUser.id,
project.id,
);
if (!isProjectManager) {
throw Errors.ATTACHMENT_NOT_FOUND; // Forbidden
}
}
const filePath = path.join(
sails.config.custom.attachmentsPath,
attachment.dirname,
attachment.filename,
);
if (!fs.existsSync(filePath)) {
throw Errors.ATTACHMENT_NOT_FOUND;
}
this.res.type(attachment.filename);
if (!attachment.isImage && path.extname(attachment.filename) !== '.pdf') {
this.res.set('Content-Disposition', 'attachment');
}
this.res.set('Cache-Control', 'private, max-age=900'); // TODO: move to config
return exits.success(fs.createReadStream(filePath));
},
};

@ -3,25 +3,36 @@ module.exports = {
const { currentUser } = this.req; const { currentUser } = this.req;
const managerProjectIds = await sails.helpers.users.getManagerProjectIds(currentUser.id); const managerProjectIds = await sails.helpers.users.getManagerProjectIds(currentUser.id);
const managerProjects = await sails.helpers.projects.getMany(managerProjectIds);
const boardMemberships = await sails.helpers.users.getBoardMemberships(currentUser.id); let boardMemberships = await sails.helpers.users.getBoardMemberships(currentUser.id);
const membershipBoardIds = await sails.helpers.utils.mapRecords(boardMemberships, 'boardId');
const membershipBoards = await sails.helpers.boards.getMany({ let membershipBoardIds = sails.helpers.utils.mapRecords(boardMemberships, 'boardId');
let membershipBoards = await sails.helpers.boards.getMany({
id: membershipBoardIds, id: membershipBoardIds,
projectId: { projectId: {
'!=': managerProjectIds, '!=': managerProjectIds,
}, },
}); });
const membershipProjectIds = sails.helpers.utils.mapRecords( let membershipProjectIds = sails.helpers.utils.mapRecords(membershipBoards, 'projectId', true);
membershipBoards, const membershipProjects = await sails.helpers.projects.getMany(membershipProjectIds);
'projectId',
true, membershipProjectIds = sails.helpers.utils.mapRecords(membershipProjects);
membershipBoards = membershipBoards.filter((membershipBoard) =>
membershipProjectIds.includes(membershipBoard.projectId),
);
membershipBoardIds = sails.helpers.utils.mapRecords(membershipBoards);
boardMemberships = boardMemberships.filter((boardMembership) =>
membershipBoardIds.includes(boardMembership.boardId),
); );
const projectIds = [...managerProjectIds, ...membershipProjectIds]; const projectIds = [...managerProjectIds, ...membershipProjectIds];
const projects = await sails.helpers.projects.getMany(projectIds); const projects = [...managerProjects, ...membershipProjects];
const projectManagers = await sails.helpers.projects.getProjectManagers(projectIds); const projectManagers = await sails.helpers.projects.getProjectManagers(projectIds);

@ -34,11 +34,16 @@ module.exports = function defineCurrentUserHook(sails) {
before: { before: {
'/*': { '/*': {
async fn(req, res, next) { async fn(req, res, next) {
const { authorization: authorizationHeader } = req.headers; let accessToken;
if (req.headers.authorization) {
if (authorizationHeader && TOKEN_PATTERN.test(authorizationHeader)) { if (TOKEN_PATTERN.test(req.headers.authorization)) {
const accessToken = authorizationHeader.replace(TOKEN_PATTERN, ''); accessToken = req.headers.authorization.replace(TOKEN_PATTERN, '');
}
} else if (req.cookies.accessToken) {
accessToken = req.cookies.accessToken;
}
if (accessToken) {
req.currentUser = await getUser(accessToken); req.currentUser = await getUser(accessToken);
} }

@ -52,9 +52,9 @@ module.exports = {
customToJSON() { customToJSON() {
return { return {
..._.omit(this, ['dirname', 'filename', 'isImage']), ..._.omit(this, ['dirname', 'filename', 'isImage']),
url: `${sails.config.custom.attachmentsUrl}/${this.dirname}/${this.filename}`, url: `${sails.config.custom.attachmentsUrl}/${this.id}/download/${this.filename}`,
coverUrl: this.isImage coverUrl: this.isImage
? `${sails.config.custom.attachmentsUrl}/${this.dirname}/thumbnails/cover-256.jpg` ? `${sails.config.custom.attachmentsUrl}/${this.id}/download/thumbnails/cover-256.jpg`
: null, : null,
}; };
}, },

@ -26,6 +26,6 @@ module.exports.custom = {
projectBackgroundImagesPath: path.join(sails.config.paths.public, 'project-background-images'), projectBackgroundImagesPath: path.join(sails.config.paths.public, 'project-background-images'),
projectBackgroundImagesUrl: `${process.env.BASE_URL}/project-background-images`, projectBackgroundImagesUrl: `${process.env.BASE_URL}/project-background-images`,
attachmentsPath: path.join(sails.config.paths.public, 'attachments'), attachmentsPath: path.join(sails.config.appPath, 'private', 'attachments'),
attachmentsUrl: `${process.env.BASE_URL}/attachments`, attachmentsUrl: `${process.env.BASE_URL}/attachments`,
}; };

@ -18,21 +18,10 @@ module.exports.policies = {
'*': 'is-authenticated', '*': 'is-authenticated',
// 'users/index': ['is-authenticated', 'is-admin'],
'users/create': ['is-authenticated', 'is-admin'], 'users/create': ['is-authenticated', 'is-admin'],
'users/delete': ['is-authenticated', 'is-admin'], 'users/delete': ['is-authenticated', 'is-admin'],
'projects/create': ['is-authenticated', 'is-admin'], 'projects/create': ['is-authenticated', 'is-admin'],
// 'projects/update': ['is-authenticated', 'is-admin'],
// 'projects/update-background-image': ['is-authenticated', 'is-admin'],
// 'projects/delete': ['is-authenticated', 'is-admin'],
// 'project-memberships/create': ['is-authenticated', 'is-admin'],
// 'project-memberships/delete': ['is-authenticated', 'is-admin'],
// 'boards/create': ['is-authenticated', 'is-admin'],
// 'boards/update': ['is-authenticated', 'is-admin'],
// 'boards/delete': ['is-authenticated', 'is-admin'],
'access-tokens/create': true, 'access-tokens/create': true,
}; };

@ -75,6 +75,16 @@ module.exports.routes = {
'GET /api/notifications/:id': 'notifications/show', 'GET /api/notifications/:id': 'notifications/show',
'PATCH /api/notifications/:ids': 'notifications/update', 'PATCH /api/notifications/:ids': 'notifications/update',
'GET /attachments/:id/download/:filename': {
action: 'attachments/download',
skipAssets: false,
},
'GET /attachments/:id/download/thumbnails/:filename': {
action: 'attachments/download-thumbnail',
skipAssets: false,
},
'GET /*': { 'GET /*': {
view: 'index', view: 'index',
skipAssets: true, skipAssets: true,

@ -31,7 +31,7 @@ module.exports.security = {
allRoutes: true, allRoutes: true,
allowOrigins: ['http://localhost:3000'], allowOrigins: ['http://localhost:3000'],
allowRequestHeaders: ['Authorization'], allowRequestHeaders: ['Authorization'],
allowCredentials: false, allowCredentials: true,
}, },
/** /**

@ -16,4 +16,4 @@ module.exports.up = (knex) =>
table.index('user_id'); table.index('user_id');
}); });
module.exports.down = (knex) => knex.schema.dropTable('project_membership'); module.exports.down = (knex) => knex.schema.dropTable('project_manager');

Loading…
Cancel
Save