Compare commits

...

11 Commits

Author SHA1 Message Date
Maksim Eltyshev
b700c307c3 chore: Update version 2024-11-12 19:14:28 +01:00
Maksim Eltyshev
9794919fd2 ref: Rename folder to dir for consistency 2024-11-12 17:07:04 +01:00
Maksim Eltyshev
917dcf31cf Merge branch 'master' of https://github.com/plankanban/planka 2024-11-12 15:58:27 +01:00
Maksim Eltyshev
850f6df0ac fix: Secure S3 attachments, bump SDK, refactoring
Closes #673
2024-11-12 15:58:22 +01:00
Jan Welslau
242d415142 fix: Use planka.fullname for postgres secret name in Helm (#939)
Closes #704
2024-11-12 14:18:36 +01:00
Nguyễn Hải Quang
950a070589 feat: Add S3 support for uploads (#938) 2024-11-11 14:59:18 +01:00
Maksim Eltyshev
f6ea10df97 chore: Update version 2024-10-31 14:58:29 +01:00
Maksim Eltyshev
d9e8c24c3f fix: Save description when clicking outside 2024-10-31 14:56:11 +01:00
Maksim Eltyshev
f75b0237d3 fix: Include previous data state in webhook
Closes #809
2024-10-31 00:48:49 +01:00
Maksim Eltyshev
38bc4cb0a0 ref: Refactoring 2024-10-30 22:28:25 +01:00
Elllone
cc95032e74 feat: Telegram bot notifications (#928) 2024-10-30 22:11:52 +01:00
48 changed files with 2480 additions and 196 deletions

View File

@@ -15,13 +15,13 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes # 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. # to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/) # Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.2.13 version: 0.2.15
# This is the version number of the application being deployed. This version number should be # 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 # 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. # follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes. # It is recommended to use it with quotes.
appVersion: "1.23.4" appVersion: "1.24.0"
dependencies: dependencies:
- alias: postgresql - alias: postgresql

View File

@@ -82,7 +82,7 @@ spec:
- name: DATABASE_URL - name: DATABASE_URL
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: planka-postgresql-svcbind-custom-user name: {{ include "planka.fullname" . }}-postgresql-svcbind-custom-user
key: uri key: uri
{{- end }} {{- end }}
- name: BASE_URL - name: BASE_URL

View File

@@ -1,8 +1,11 @@
import React, { useCallback, useImperativeHandle, useMemo, useState } from 'react'; import React, { useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, Form } from 'semantic-ui-react'; import { Button, Form } from 'semantic-ui-react';
import SimpleMDE from 'react-simplemde-editor'; import SimpleMDE from 'react-simplemde-editor';
import { useClickAwayListener } from '../../lib/hooks';
import { useNestedRef } from '../../hooks';
import styles from './DescriptionEdit.module.scss'; import styles from './DescriptionEdit.module.scss';
@@ -11,6 +14,10 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate },
const [isOpened, setIsOpened] = useState(false); const [isOpened, setIsOpened] = useState(false);
const [value, setValue] = useState(null); const [value, setValue] = useState(null);
const editorWrapperRef = useRef(null);
const codemirrorRef = useRef(null);
const [buttonRef, handleButtonRef] = useNestedRef();
const open = useCallback(() => { const open = useCallback(() => {
setIsOpened(true); setIsOpened(true);
setValue(defaultValue || ''); setValue(defaultValue || '');
@@ -55,6 +62,28 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate },
close(); close();
}, [close]); }, [close]);
const handleAwayClick = useCallback(() => {
if (!isOpened) {
return;
}
close();
}, [isOpened, close]);
const handleClickAwayCancel = useCallback(() => {
codemirrorRef.current.focus();
}, []);
const clickAwayProps = useClickAwayListener(
[editorWrapperRef, buttonRef],
handleAwayClick,
handleClickAwayCancel,
);
const handleGetCodemirrorInstance = useCallback((codemirror) => {
codemirrorRef.current = codemirror;
}, []);
const mdEditorOptions = useMemo( const mdEditorOptions = useMemo(
() => ({ () => ({
autoDownloadFontAwesome: false, autoDownloadFontAwesome: false,
@@ -92,16 +121,20 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate },
return ( return (
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<div {...clickAwayProps} ref={editorWrapperRef}>
<SimpleMDE <SimpleMDE
value={value} value={value}
options={mdEditorOptions} options={mdEditorOptions}
placeholder={t('common.enterDescription')} placeholder={t('common.enterDescription')}
className={styles.field} className={styles.field}
getCodemirrorInstance={handleGetCodemirrorInstance}
onKeyDown={handleFieldKeyDown} onKeyDown={handleFieldKeyDown}
onChange={setValue} onChange={setValue}
/> />
</div>
<div className={styles.controls}> <div className={styles.controls}>
<Button positive content={t('action.save')} /> <Button positive ref={handleButtonRef} content={t('action.save')} />
</div> </div>
</Form> </Form>
); );

View File

@@ -1,7 +1,8 @@
import useNestedRef from './use-nested-ref';
import useField from './use-field'; import useField from './use-field';
import useForm from './use-form'; import useForm from './use-form';
import useSteps from './use-steps'; import useSteps from './use-steps';
import useModal from './use-modal'; import useModal from './use-modal';
import useClosableForm from './use-closable-form'; import useClosableForm from './use-closable-form';
export { useField, useForm, useSteps, useModal, useClosableForm }; export { useNestedRef, useField, useForm, useSteps, useModal, useClosableForm };

View File

@@ -0,0 +1,14 @@
import { useCallback, useRef } from 'react';
export default (nestedRefName = 'ref') => {
const ref = useRef(null);
const handleRef = useCallback(
(element) => {
ref.current = element?.[nestedRefName].current;
},
[nestedRefName],
);
return [ref, handleRef];
};

View File

@@ -2,5 +2,6 @@ import usePrevious from './use-previous';
import useToggle from './use-toggle'; import useToggle from './use-toggle';
import useForceUpdate from './use-force-update'; import useForceUpdate from './use-force-update';
import useDidUpdate from './use-did-update'; import useDidUpdate from './use-did-update';
import useClickAwayListener from './use-click-away-listener';
export { usePrevious, useToggle, useForceUpdate, useDidUpdate }; export { usePrevious, useToggle, useForceUpdate, useDidUpdate, useClickAwayListener };

View File

@@ -0,0 +1,45 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
export default (elementRefs, onAwayClick, onCancel) => {
const pressedElement = useRef(null);
const handlePress = useCallback((event) => {
pressedElement.current = event.target;
}, []);
useEffect(() => {
const handleEvent = (event) => {
const element = elementRefs.find(({ current }) => current?.contains(event.target))?.current;
if (element) {
if (!pressedElement.current || pressedElement.current !== element) {
onCancel();
}
} else if (pressedElement.current) {
onCancel();
} else {
onAwayClick();
}
pressedElement.current = null;
};
document.addEventListener('mouseup', handleEvent, true);
document.addEventListener('touchend', handleEvent, true);
return () => {
document.removeEventListener('mouseup', handleEvent, true);
document.removeEventListener('touchend', handleEvent, true);
};
}, [onAwayClick, onCancel]); // eslint-disable-line react-hooks/exhaustive-deps
const props = useMemo(
() => ({
onMouseDown: handlePress,
onTouchStart: handlePress,
}),
[handlePress],
);
return props;
};

View File

@@ -1 +1 @@
export default '1.23.4'; export default '1.24.0';

View File

@@ -25,9 +25,15 @@ services:
# - KNEX_REJECT_UNAUTHORIZED_SSL_CERTIFICATE=false # - KNEX_REJECT_UNAUTHORIZED_SSL_CERTIFICATE=false
# - SHOW_DETAILED_AUTH_ERRORS=false # Set to true to show more detailed authentication error messages. It should not be enabled without a rate limiter for security reasons. # - SHOW_DETAILED_AUTH_ERRORS=false # Set to true to show more detailed authentication error messages. It should not be enabled without a rate limiter for security reasons.
# - ALLOW_ALL_TO_CREATE_PROJECTS=true # - ALLOW_ALL_TO_CREATE_PROJECTS=true
# - S3_ENDPOINT=
# - S3_REGION=
# - S3_ACCESS_KEY_ID=
# - S3_SECRET_ACCESS_KEY=
# - S3_BUCKET=
# - S3_FORCE_PATH_STYLE=true
# - OIDC_ISSUER= # - OIDC_ISSUER=
# - OIDC_CLIENT_ID= # - OIDC_CLIENT_ID=
# - OIDC_CLIENT_SECRET= # - OIDC_CLIENT_SECRET=
@@ -67,6 +73,12 @@ services:
# - SLACK_BOT_TOKEN= # - SLACK_BOT_TOKEN=
# - SLACK_CHANNEL_ID= # - SLACK_CHANNEL_ID=
# - GOOGLE_CHAT_WEBHOOK_URL=
# - TELEGRAM_BOT_TOKEN=
# - TELEGRAM_CHAT_ID=
# - TELEGRAM_THREAD_ID=
working_dir: /app working_dir: /app
command: ["sh", "-c", "npm run start"] command: ["sh", "-c", "npm run start"]
depends_on: depends_on:

View File

@@ -32,9 +32,15 @@ services:
# - DEFAULT_ADMIN_USERNAME=demo # - DEFAULT_ADMIN_USERNAME=demo
# - SHOW_DETAILED_AUTH_ERRORS=false # Set to true to show more detailed authentication error messages. It should not be enabled without a rate limiter for security reasons. # - SHOW_DETAILED_AUTH_ERRORS=false # Set to true to show more detailed authentication error messages. It should not be enabled without a rate limiter for security reasons.
# - ALLOW_ALL_TO_CREATE_PROJECTS=true # - ALLOW_ALL_TO_CREATE_PROJECTS=true
# - S3_ENDPOINT=
# - S3_REGION=
# - S3_ACCESS_KEY_ID=
# - S3_SECRET_ACCESS_KEY=
# - S3_BUCKET=
# - S3_FORCE_PATH_STYLE=true
# - OIDC_ISSUER= # - OIDC_ISSUER=
# - OIDC_CLIENT_ID= # - OIDC_CLIENT_ID=
# - OIDC_CLIENT_SECRET= # - OIDC_CLIENT_SECRET=
@@ -74,7 +80,12 @@ services:
# - SLACK_BOT_TOKEN= # - SLACK_BOT_TOKEN=
# - SLACK_CHANNEL_ID= # - SLACK_CHANNEL_ID=
# - GOOGLE_CHAT_WEBHOOK_URL= # - GOOGLE_CHAT_WEBHOOK_URL=
# - TELEGRAM_BOT_TOKEN=
# - TELEGRAM_CHAT_ID=
# - TELEGRAM_THREAD_ID=
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy

4
package-lock.json generated
View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "planka", "name": "planka",
"version": "1.23.4", "version": "1.24.0",
"private": true, "private": true,
"homepage": "https://plankanban.github.io/planka", "homepage": "https://plankanban.github.io/planka",
"repository": { "repository": {
@@ -16,7 +16,7 @@
"client:test": "npm test --prefix client", "client:test": "npm test --prefix client",
"docker:build": "docker build -t ghcr.io/plankanban/planka:local -f Dockerfile .", "docker:build": "docker build -t ghcr.io/plankanban/planka:local -f Dockerfile .",
"docker:build:base": "docker build -t ghcr.io/plankanban/planka:base-local -f Dockerfile.base .", "docker:build:base": "docker build -t ghcr.io/plankanban/planka:base-local -f Dockerfile.base .",
"gv": "genversion --source ./ --template client/version-template.ejs client/src/version.js", "gv": "npm i --package-lock-only --ignore-scripts && genversion --source ./ --template client/version-template.ejs client/src/version.js",
"postinstall": "(cd server && npm i && cd ../client && npm i)", "postinstall": "(cd server && npm i && cd ../client && npm i)",
"lint": "npm run server:lint && npm run client:lint", "lint": "npm run server:lint && npm run client:lint",
"prepare": "husky", "prepare": "husky",

View File

@@ -25,9 +25,15 @@ SECRET_KEY=notsecretkey
# DEFAULT_ADMIN_USERNAME=demo # DEFAULT_ADMIN_USERNAME=demo
# SHOW_DETAILED_AUTH_ERRORS=false # Set to true to show more detailed authentication error messages. It should not be enabled without a rate limiter for security reasons. # SHOW_DETAILED_AUTH_ERRORS=false # Set to true to show more detailed authentication error messages. It should not be enabled without a rate limiter for security reasons.
# ALLOW_ALL_TO_CREATE_PROJECTS=true # ALLOW_ALL_TO_CREATE_PROJECTS=true
# S3_ENDPOINT=
# S3_REGION=
# S3_ACCESS_KEY_ID=
# S3_SECRET_ACCESS_KEY=
# S3_BUCKET=
# S3_FORCE_PATH_STYLE=true
# OIDC_ISSUER= # OIDC_ISSUER=
# OIDC_CLIENT_ID= # OIDC_CLIENT_ID=
# OIDC_CLIENT_SECRET= # OIDC_CLIENT_SECRET=
@@ -65,8 +71,13 @@ SECRET_KEY=notsecretkey
# SLACK_BOT_TOKEN= # SLACK_BOT_TOKEN=
# SLACK_CHANNEL_ID= # SLACK_CHANNEL_ID=
# GOOGLE_CHAT_WEBHOOK_URL= # GOOGLE_CHAT_WEBHOOK_URL=
# TELEGRAM_BOT_TOKEN=
# TELEGRAM_CHAT_ID=
# TELEGRAM_THREAD_ID=
## Do not edit this ## Do not edit this
TZ=UTC TZ=UTC

View File

@@ -1,6 +1,3 @@
const fs = require('fs');
const path = require('path');
const Errors = { const Errors = {
ATTACHMENT_NOT_FOUND: { ATTACHMENT_NOT_FOUND: {
attachmentNotFound: 'Attachment not found', attachmentNotFound: 'Attachment not found',
@@ -46,20 +43,20 @@ module.exports = {
throw Errors.ATTACHMENT_NOT_FOUND; throw Errors.ATTACHMENT_NOT_FOUND;
} }
const filePath = path.join( const fileManager = sails.hooks['file-manager'].getInstance();
sails.config.custom.attachmentsPath,
attachment.dirname,
'thumbnails',
`cover-256.${attachment.image.thumbnailsExtension}`,
);
if (!fs.existsSync(filePath)) { let readStream;
try {
readStream = await fileManager.read(
`${sails.config.custom.attachmentsPathSegment}/${attachment.dirname}/thumbnails/cover-256.${attachment.image.thumbnailsExtension}`,
);
} catch (error) {
throw Errors.ATTACHMENT_NOT_FOUND; throw Errors.ATTACHMENT_NOT_FOUND;
} }
this.res.type('image/jpeg'); this.res.type('image/jpeg');
this.res.set('Cache-Control', 'private, max-age=900'); // TODO: move to config this.res.set('Cache-Control', 'private, max-age=900'); // TODO: move to config
return exits.success(fs.createReadStream(filePath)); return exits.success(readStream);
}, },
}; };

View File

@@ -1,4 +1,3 @@
const fs = require('fs');
const path = require('path'); const path = require('path');
const Errors = { const Errors = {
@@ -42,13 +41,14 @@ module.exports = {
} }
} }
const filePath = path.join( const fileManager = sails.hooks['file-manager'].getInstance();
sails.config.custom.attachmentsPath,
attachment.dirname,
attachment.filename,
);
if (!fs.existsSync(filePath)) { let readStream;
try {
readStream = await fileManager.read(
`${sails.config.custom.attachmentsPathSegment}/${attachment.dirname}/${attachment.filename}`,
);
} catch (error) {
throw Errors.ATTACHMENT_NOT_FOUND; throw Errors.ATTACHMENT_NOT_FOUND;
} }
@@ -58,6 +58,6 @@ module.exports = {
} }
this.res.set('Cache-Control', 'private, max-age=900'); // TODO: move to config this.res.set('Cache-Control', 'private, max-age=900'); // TODO: move to config
return exits.success(fs.createReadStream(filePath)); return exits.success(readStream);
}, },
}; };

View File

@@ -14,7 +14,10 @@ const valuesValidator = (value) => {
return true; return true;
}; };
const buildAndSendMessage = async (card, action, actorUser, send) => { const truncateString = (string, maxLength = 30) =>
string.length > maxLength ? `${string.substring(0, 30)}...` : string;
const buildAndSendMarkdownMessage = async (card, action, actorUser, send) => {
const cardLink = `<${sails.config.custom.baseUrl}/cards/${card.id}|${card.name}>`; const cardLink = `<${sails.config.custom.baseUrl}/cards/${card.id}|${card.name}>`;
let markdown; let markdown;
@@ -28,6 +31,7 @@ const buildAndSendMessage = async (card, action, actorUser, send) => {
break; break;
case Action.Types.COMMENT_CARD: case Action.Types.COMMENT_CARD:
// TODO: truncate text?
markdown = `*${actorUser.name}* commented on ${cardLink}:\n>${action.data.text}`; markdown = `*${actorUser.name}* commented on ${cardLink}:\n>${action.data.text}`;
break; break;
@@ -38,6 +42,31 @@ const buildAndSendMessage = async (card, action, actorUser, send) => {
await send(markdown); await send(markdown);
}; };
const buildAndSendHtmlMessage = async (card, action, actorUser, send) => {
const cardLink = `<a href="${sails.config.custom.baseUrl}/cards/${card.id}">${card.name}</a>`;
let html;
switch (action.type) {
case Action.Types.CREATE_CARD:
html = `${cardLink} was created by ${actorUser.name} in <b>${action.data.list.name}</b>`;
break;
case Action.Types.MOVE_CARD:
html = `${cardLink} was moved by ${actorUser.name} to <b>${action.data.toList.name}</b>`;
break;
case Action.Types.COMMENT_CARD: {
html = `<b>${actorUser.name}</b> commented on ${cardLink}:\n<i>${truncateString(action.data.text)}</i>`;
break;
}
default:
return;
}
await send(html);
};
module.exports = { module.exports = {
inputs: { inputs: {
values: { values: {
@@ -116,17 +145,32 @@ module.exports = {
); );
if (sails.config.custom.slackBotToken) { if (sails.config.custom.slackBotToken) {
buildAndSendMessage(values.card, action, values.user, sails.helpers.utils.sendSlackMessage); buildAndSendMarkdownMessage(
values.card,
action,
values.user,
sails.helpers.utils.sendSlackMessage,
);
} }
if (sails.config.custom.googleChatWebhookUrl) { if (sails.config.custom.googleChatWebhookUrl) {
buildAndSendMessage( buildAndSendMarkdownMessage(
values.card, values.card,
action, action,
values.user, values.user,
sails.helpers.utils.sendGoogleChatMessage, sails.helpers.utils.sendGoogleChatMessage,
); );
} }
if (sails.config.custom.telegramBotToken) {
buildAndSendHtmlMessage(
values.card,
action,
values.user,
sails.helpers.utils.sendTelegramMessage,
);
}
return action; return action;
}, },
}; };

View File

@@ -59,6 +59,9 @@ module.exports = {
cards: [inputs.card], cards: [inputs.card],
}, },
}, },
prevData: {
item: inputs.record,
},
user: inputs.actorUser, user: inputs.actorUser,
}); });
} }

View File

@@ -1,6 +1,3 @@
const path = require('path');
const rimraf = require('rimraf');
module.exports = { module.exports = {
inputs: { inputs: {
record: { record: {
@@ -50,8 +47,12 @@ module.exports = {
const attachment = await Attachment.archiveOne(inputs.record.id); const attachment = await Attachment.archiveOne(inputs.record.id);
if (attachment) { if (attachment) {
const fileManager = sails.hooks['file-manager'].getInstance();
try { try {
rimraf.sync(path.join(sails.config.custom.attachmentsPath, attachment.dirname)); await fileManager.deleteDir(
`${sails.config.custom.attachmentsPathSegment}/${attachment.dirname}`,
);
} catch (error) { } catch (error) {
console.warn(error.stack); // eslint-disable-line no-console console.warn(error.stack); // eslint-disable-line no-console
} }

View File

@@ -1,7 +1,4 @@
const fs = require('fs'); const { rimraf } = require('rimraf');
const path = require('path');
const rimraf = require('rimraf');
const moveFile = require('move-file');
const { v4: uuid } = require('uuid'); const { v4: uuid } = require('uuid');
const sharp = require('sharp'); const sharp = require('sharp');
@@ -16,17 +13,19 @@ module.exports = {
}, },
async fn(inputs) { async fn(inputs) {
const dirname = uuid(); const fileManager = sails.hooks['file-manager'].getInstance();
const dirname = uuid();
const dirPathSegment = `${sails.config.custom.attachmentsPathSegment}/${dirname}`;
const filename = filenamify(inputs.file.filename); const filename = filenamify(inputs.file.filename);
const rootPath = path.join(sails.config.custom.attachmentsPath, dirname); const filePath = await fileManager.move(
const filePath = path.join(rootPath, filename); inputs.file.fd,
`${dirPathSegment}/${filename}`,
inputs.file.type,
);
fs.mkdirSync(rootPath); let image = sharp(filePath || inputs.file.fd, {
await moveFile(inputs.file.fd, filePath);
let image = sharp(filePath, {
animated: true, animated: true,
}); });
@@ -43,9 +42,6 @@ module.exports = {
}; };
if (metadata && !['svg', 'pdf'].includes(metadata.format)) { if (metadata && !['svg', 'pdf'].includes(metadata.format)) {
const thumbnailsPath = path.join(rootPath, 'thumbnails');
fs.mkdirSync(thumbnailsPath);
let { width, pageHeight: height = metadata.height } = metadata; let { width, pageHeight: height = metadata.height } = metadata;
if (metadata.orientation && metadata.orientation > 4) { if (metadata.orientation && metadata.orientation > 4) {
[image, width, height] = [image.rotate(), height, width]; [image, width, height] = [image.rotate(), height, width];
@@ -55,7 +51,7 @@ module.exports = {
const thumbnailsExtension = metadata.format === 'jpeg' ? 'jpg' : metadata.format; const thumbnailsExtension = metadata.format === 'jpeg' ? 'jpg' : metadata.format;
try { try {
await image const resizeBuffer = await image
.resize( .resize(
256, 256,
isPortrait ? 320 : undefined, isPortrait ? 320 : undefined,
@@ -65,20 +61,30 @@ module.exports = {
} }
: undefined, : undefined,
) )
.toFile(path.join(thumbnailsPath, `cover-256.${thumbnailsExtension}`)); .toBuffer();
await fileManager.save(
`${dirPathSegment}/thumbnails/cover-256.${thumbnailsExtension}`,
resizeBuffer,
inputs.file.type,
);
fileData.image = { fileData.image = {
width, width,
height, height,
thumbnailsExtension, thumbnailsExtension,
}; };
} catch (error1) { } catch (error) {
try { console.warn(error.stack); // eslint-disable-line no-console
rimraf.sync(thumbnailsPath);
} catch (error2) {
console.warn(error2.stack); // eslint-disable-line no-console
} }
} }
if (!filePath) {
try {
await rimraf(inputs.file.fd);
} catch (error) {
console.warn(error.stack); // eslint-disable-line no-console
}
} }
return fileData; return fileData;

View File

@@ -59,6 +59,9 @@ module.exports = {
cards: [inputs.card], cards: [inputs.card],
}, },
}, },
prevData: {
item: inputs.record,
},
user: inputs.actorUser, user: inputs.actorUser,
}); });
} }

View File

@@ -62,6 +62,9 @@ module.exports = {
boards: [inputs.board], boards: [inputs.board],
}, },
}, },
prevData: {
item: inputs.record,
},
user: inputs.actorUser, user: inputs.actorUser,
}); });
} }

View File

@@ -1,5 +1,5 @@
const fs = require('fs').promises; const fs = require('fs');
const rimraf = require('rimraf'); const { rimraf } = require('rimraf');
module.exports = { module.exports = {
inputs: { inputs: {
@@ -14,7 +14,7 @@ module.exports = {
}, },
async fn(inputs) { async fn(inputs) {
const content = await fs.readFile(inputs.file.fd); const content = await fs.promises.readFile(inputs.file.fd);
const trelloBoard = JSON.parse(content); const trelloBoard = JSON.parse(content);
if ( if (
@@ -28,7 +28,7 @@ module.exports = {
} }
try { try {
rimraf.sync(inputs.file.fd); await rimraf(inputs.file.fd);
} catch (error) { } catch (error) {
console.warn(error.stack); // eslint-disable-line no-console console.warn(error.stack); // eslint-disable-line no-console
} }

View File

@@ -100,6 +100,9 @@ module.exports = {
projects: [inputs.project], projects: [inputs.project],
}, },
}, },
prevData: {
item: inputs.record,
},
user: inputs.actorUser, user: inputs.actorUser,
}); });
} }

View File

@@ -1,7 +1,11 @@
const buildAndSendMessage = async (card, actorUser, send) => { const buildAndSendMarkdownMessage = async (card, actorUser, send) => {
await send(`*${card.name}* was deleted by ${actorUser.name}`); await send(`*${card.name}* was deleted by ${actorUser.name}`);
}; };
const buildAndSendHtmlMessage = async (card, actorUser, send) => {
await send(`<b>${card.name}</b> was deleted by ${actorUser.name}`);
};
module.exports = { module.exports = {
inputs: { inputs: {
record: { record: {
@@ -56,11 +60,19 @@ module.exports = {
}); });
if (sails.config.custom.slackBotToken) { if (sails.config.custom.slackBotToken) {
buildAndSendMessage(card, inputs.actorUser, sails.helpers.utils.sendSlackMessage); buildAndSendMarkdownMessage(card, inputs.actorUser, sails.helpers.utils.sendSlackMessage);
} }
if (sails.config.custom.googleChatWebhookUrl) { if (sails.config.custom.googleChatWebhookUrl) {
buildAndSendMessage(card, inputs.actorUser, sails.helpers.utils.sendGoogleChatMessage); buildAndSendMarkdownMessage(
card,
inputs.actorUser,
sails.helpers.utils.sendGoogleChatMessage,
);
}
if (sails.config.custom.telegramBotToken) {
buildAndSendHtmlMessage(card, inputs.actorUser, sails.helpers.utils.sendTelegramMessage);
} }
} }

View File

@@ -264,6 +264,9 @@ module.exports = {
lists: [list], lists: [list],
}, },
}, },
prevData: {
item: inputs.record,
},
user: inputs.actorUser, user: inputs.actorUser,
}); });

View File

@@ -91,6 +91,9 @@ module.exports = {
boards: [inputs.board], boards: [inputs.board],
}, },
}, },
prevData: {
item: inputs.record,
},
user: inputs.actorUser, user: inputs.actorUser,
}); });
} }

View File

@@ -91,6 +91,9 @@ module.exports = {
boards: [inputs.board], boards: [inputs.board],
}, },
}, },
prevData: {
item: inputs.record,
},
user: inputs.actorUser, user: inputs.actorUser,
}); });
} }

View File

@@ -48,6 +48,7 @@ module.exports = {
inputs.request, inputs.request,
); );
// TODO: with prevData?
sails.helpers.utils.sendWebhooks.with({ sails.helpers.utils.sendWebhooks.with({
event: 'notificationUpdate', event: 'notificationUpdate',
data: { data: {

View File

@@ -1,6 +1,4 @@
const fs = require('fs'); const { rimraf } = require('rimraf');
const path = require('path');
const rimraf = require('rimraf');
const { v4: uuid } = require('uuid'); const { v4: uuid } = require('uuid');
const sharp = require('sharp'); const sharp = require('sharp');
@@ -32,10 +30,10 @@ module.exports = {
throw 'fileIsNotImage'; throw 'fileIsNotImage';
} }
const dirname = uuid(); const fileManager = sails.hooks['file-manager'].getInstance();
const rootPath = path.join(sails.config.custom.projectBackgroundImagesPath, dirname);
fs.mkdirSync(rootPath); const dirname = uuid();
const dirPathSegment = `${sails.config.custom.projectBackgroundImagesPathSegment}/${dirname}`;
let { width, pageHeight: height = metadata.height } = metadata; let { width, pageHeight: height = metadata.height } = metadata;
if (metadata.orientation && metadata.orientation > 4) { if (metadata.orientation && metadata.orientation > 4) {
@@ -45,9 +43,15 @@ module.exports = {
const extension = metadata.format === 'jpeg' ? 'jpg' : metadata.format; const extension = metadata.format === 'jpeg' ? 'jpg' : metadata.format;
try { try {
await image.toFile(path.join(rootPath, `original.${extension}`)); const originalBuffer = await image.toBuffer();
await image await fileManager.save(
`${dirPathSegment}/original.${extension}`,
originalBuffer,
inputs.file.type,
);
const cover336Buffer = await image
.resize( .resize(
336, 336,
200, 200,
@@ -57,10 +61,18 @@ module.exports = {
} }
: undefined, : undefined,
) )
.toFile(path.join(rootPath, `cover-336.${extension}`)); .toBuffer();
await fileManager.save(
`${dirPathSegment}/cover-336.${extension}`,
cover336Buffer,
inputs.file.type,
);
} catch (error1) { } catch (error1) {
console.warn(error1.stack); // eslint-disable-line no-console
try { try {
rimraf.sync(rootPath); fileManager.deleteDir(dirPathSegment);
} catch (error2) { } catch (error2) {
console.warn(error2.stack); // eslint-disable-line no-console console.warn(error2.stack); // eslint-disable-line no-console
} }
@@ -69,7 +81,7 @@ module.exports = {
} }
try { try {
rimraf.sync(inputs.file.fd); await rimraf(inputs.file.fd);
} catch (error) { } catch (error) {
console.warn(error.stack); // eslint-disable-line no-console console.warn(error.stack); // eslint-disable-line no-console
} }

View File

@@ -1,6 +1,3 @@
const path = require('path');
const rimraf = require('rimraf');
const valuesValidator = (value) => { const valuesValidator = (value) => {
if (!_.isPlainObject(value)) { if (!_.isPlainObject(value)) {
return false; return false;
@@ -86,12 +83,11 @@ module.exports = {
(!project.backgroundImage || (!project.backgroundImage ||
project.backgroundImage.dirname !== inputs.record.backgroundImage.dirname) project.backgroundImage.dirname !== inputs.record.backgroundImage.dirname)
) { ) {
const fileManager = sails.hooks['file-manager'].getInstance();
try { try {
rimraf.sync( await fileManager.deleteDir(
path.join( `${sails.config.custom.projectBackgroundImagesPathSegment}/${inputs.record.backgroundImage.dirname}`,
sails.config.custom.projectBackgroundImagesPath,
inputs.record.backgroundImage.dirname,
),
); );
} catch (error) { } catch (error) {
console.warn(error.stack); // eslint-disable-line no-console console.warn(error.stack); // eslint-disable-line no-console
@@ -118,6 +114,9 @@ module.exports = {
data: { data: {
item: project, item: project,
}, },
prevData: {
item: inputs.record,
},
user: inputs.actorUser, user: inputs.actorUser,
}); });
} }

View File

@@ -101,6 +101,9 @@ module.exports = {
cards: [inputs.card], cards: [inputs.card],
}, },
}, },
prevData: {
item: inputs.record,
},
user: inputs.actorUser, user: inputs.actorUser,
}); });
} }

View File

@@ -1,6 +1,4 @@
const fs = require('fs'); const { rimraf } = require('rimraf');
const path = require('path');
const rimraf = require('rimraf');
const { v4: uuid } = require('uuid'); const { v4: uuid } = require('uuid');
const sharp = require('sharp'); const sharp = require('sharp');
@@ -32,10 +30,10 @@ module.exports = {
throw 'fileIsNotImage'; throw 'fileIsNotImage';
} }
const dirname = uuid(); const fileManager = sails.hooks['file-manager'].getInstance();
const rootPath = path.join(sails.config.custom.userAvatarsPath, dirname);
fs.mkdirSync(rootPath); const dirname = uuid();
const dirPathSegment = `${sails.config.custom.userAvatarsPathSegment}/${dirname}`;
let { width, pageHeight: height = metadata.height } = metadata; let { width, pageHeight: height = metadata.height } = metadata;
if (metadata.orientation && metadata.orientation > 4) { if (metadata.orientation && metadata.orientation > 4) {
@@ -45,9 +43,15 @@ module.exports = {
const extension = metadata.format === 'jpeg' ? 'jpg' : metadata.format; const extension = metadata.format === 'jpeg' ? 'jpg' : metadata.format;
try { try {
await image.toFile(path.join(rootPath, `original.${extension}`)); const originalBuffer = await image.toBuffer();
await image await fileManager.save(
`${dirPathSegment}/original.${extension}`,
originalBuffer,
inputs.file.type,
);
const square100Buffer = await image
.resize( .resize(
100, 100,
100, 100,
@@ -57,10 +61,18 @@ module.exports = {
} }
: undefined, : undefined,
) )
.toFile(path.join(rootPath, `square-100.${extension}`)); .toBuffer();
await fileManager.save(
`${dirPathSegment}/square-100.${extension}`,
square100Buffer,
inputs.file.type,
);
} catch (error1) { } catch (error1) {
console.warn(error1.stack); // eslint-disable-line no-console
try { try {
rimraf.sync(rootPath); fileManager.deleteDir(dirPathSegment);
} catch (error2) { } catch (error2) {
console.warn(error2.stack); // eslint-disable-line no-console console.warn(error2.stack); // eslint-disable-line no-console
} }
@@ -69,7 +81,7 @@ module.exports = {
} }
try { try {
rimraf.sync(inputs.file.fd); await rimraf(inputs.file.fd);
} catch (error) { } catch (error) {
console.warn(error.stack); // eslint-disable-line no-console console.warn(error.stack); // eslint-disable-line no-console
} }

View File

@@ -1,6 +1,4 @@
const path = require('path');
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
const rimraf = require('rimraf');
const { v4: uuid } = require('uuid'); const { v4: uuid } = require('uuid');
const valuesValidator = (value) => { const valuesValidator = (value) => {
@@ -101,8 +99,12 @@ module.exports = {
inputs.record.avatar && inputs.record.avatar &&
(!user.avatar || user.avatar.dirname !== inputs.record.avatar.dirname) (!user.avatar || user.avatar.dirname !== inputs.record.avatar.dirname)
) { ) {
const fileManager = sails.hooks['file-manager'].getInstance();
try { try {
rimraf.sync(path.join(sails.config.custom.userAvatarsPath, inputs.record.avatar.dirname)); await fileManager.deleteDir(
`${sails.config.custom.userAvatarsPathSegment}/${inputs.record.avatar.dirname}`,
);
} catch (error) { } catch (error) {
console.warn(error.stack); // eslint-disable-line no-console console.warn(error.stack); // eslint-disable-line no-console
} }
@@ -159,6 +161,9 @@ module.exports = {
data: { data: {
item: user, item: user,
}, },
prevData: {
item: inputs.record,
},
user: inputs.actorUser, user: inputs.actorUser,
}); });
} }

View File

@@ -4,7 +4,7 @@ const { v4: uuid } = require('uuid');
async function doUpload(paramName, req, options) { async function doUpload(paramName, req, options) {
const uploadOptions = { const uploadOptions = {
...options, ...options,
dirname: options.dirname || sails.config.custom.fileUploadTmpDir, dirname: options.dirname || sails.config.custom.uploadsTempPath,
}; };
const upload = util.promisify((opts, callback) => { const upload = util.promisify((opts, callback) => {
return req.file(paramName).upload(opts, (error, files) => callback(error, files)); return req.file(paramName).upload(opts, (error, files) => callback(error, files));
@@ -33,7 +33,7 @@ module.exports = {
exits.success( exits.success(
await doUpload(inputs.paramName, inputs.req, { await doUpload(inputs.paramName, inputs.req, {
saveAs: uuid(), saveAs: uuid(),
dirname: sails.config.custom.fileUploadTmpDir, dirname: sails.config.custom.uploadsTempPath,
maxBytes: null, maxBytes: null,
}), }),
); );

View File

@@ -0,0 +1,44 @@
const buildSendMessageApiUrl = (telegramBotToken) =>
`https://api.telegram.org/bot${telegramBotToken}/sendMessage`;
module.exports = {
inputs: {
html: {
type: 'string',
required: true,
},
},
async fn(inputs) {
const headers = {
'Content-Type': 'application/json; charset=utf-8',
};
const body = {
chat_id: sails.config.custom.telegramChatId,
text: inputs.html,
parse_mode: 'HTML',
};
if (sails.config.custom.telegramThreadId) {
body.message_thread_id = sails.config.custom.telegramThreadId;
}
let response;
try {
response = await fetch(buildSendMessageApiUrl(sails.config.custom.telegramBotToken), {
headers,
method: 'POST',
body: JSON.stringify(body),
});
} catch (error) {
sails.log.error(`Error sending to Telegram: ${error}`);
return;
}
if (!response.ok) {
const responseJson = await response.json();
sails.log.error(`Error sending to Telegram: ${responseJson.description}`);
}
},
};

View File

@@ -97,10 +97,11 @@ const jsonifyData = (data) => {
* @param {*} webhook - Webhook configuration. * @param {*} webhook - Webhook configuration.
* @param {string} event - The event (see {@link EVENT_TYPES}). * @param {string} event - The event (see {@link EVENT_TYPES}).
* @param {Data} data - The data object containing event data and optionally included data. * @param {Data} data - The data object containing event data and optionally included data.
* @param {Data} [prevData] - The data object containing previous state of data (optional).
* @param {ref} user - User object associated with the event. * @param {ref} user - User object associated with the event.
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function sendWebhook(webhook, event, data, user) { async function sendWebhook(webhook, event, data, prevData, user) {
const headers = { const headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'User-Agent': `planka (+${sails.config.custom.baseUrl})`, 'User-Agent': `planka (+${sails.config.custom.baseUrl})`,
@@ -113,6 +114,7 @@ async function sendWebhook(webhook, event, data, user) {
const body = JSON.stringify({ const body = JSON.stringify({
event, event,
data: jsonifyData(data), data: jsonifyData(data),
prevData: prevData && jsonifyData(prevData),
user: sails.helpers.utils.jsonifyRecord(user), user: sails.helpers.utils.jsonifyRecord(user),
}); });
@@ -148,6 +150,9 @@ module.exports = {
type: 'ref', type: 'ref',
required: true, required: true,
}, },
prevData: {
type: 'ref',
},
user: { user: {
type: 'ref', type: 'ref',
required: true, required: true,
@@ -172,7 +177,7 @@ module.exports = {
return; return;
} }
sendWebhook(webhook, inputs.event, inputs.data, inputs.user); sendWebhook(webhook, inputs.event, inputs.data, inputs.prevData, inputs.user);
}); });
}, },
}; };

View File

@@ -0,0 +1,51 @@
const fs = require('fs');
const fse = require('fs-extra');
const path = require('path');
const { rimraf } = require('rimraf');
const PATH_SEGMENT_TO_URL_REPLACE_REGEX = /(public|private)\//;
const buildPath = (pathSegment) => path.join(sails.config.custom.uploadsBasePath, pathSegment);
class LocalFileManager {
// eslint-disable-next-line class-methods-use-this
async move(sourceFilePath, filePathSegment) {
const { dir, base } = path.parse(filePathSegment);
const dirPath = buildPath(dir);
const filePath = path.join(dirPath, base);
await fs.promises.mkdir(dirPath);
await fse.move(sourceFilePath, filePath);
return filePath;
}
// eslint-disable-next-line class-methods-use-this
async save(filePathSegment, buffer) {
await fse.outputFile(buildPath(filePathSegment), buffer);
}
// eslint-disable-next-line class-methods-use-this
read(filePathSegment) {
const filePath = buildPath(filePathSegment);
if (!fs.existsSync(filePath)) {
throw new Error('File does not exist');
}
return fs.createReadStream(filePath);
}
// eslint-disable-next-line class-methods-use-this
async deleteDir(dirPathSegment) {
await rimraf(buildPath(dirPathSegment));
}
// eslint-disable-next-line class-methods-use-this
buildUrl(filePathSegment) {
return `${sails.config.custom.baseUrl}/${filePathSegment.replace(PATH_SEGMENT_TO_URL_REPLACE_REGEX, '')}`;
}
}
module.exports = LocalFileManager;

View File

@@ -0,0 +1,76 @@
const fs = require('fs');
const {
DeleteObjectsCommand,
GetObjectCommand,
ListObjectsV2Command,
PutObjectCommand,
} = require('@aws-sdk/client-s3');
class S3FileManager {
constructor(client) {
this.client = client;
}
async move(sourceFilePath, filePathSegment, contentType) {
const command = new PutObjectCommand({
Bucket: sails.config.custom.s3Bucket,
Key: filePathSegment,
Body: fs.createReadStream(sourceFilePath),
ContentType: contentType,
});
await this.client.send(command);
return null;
}
async save(filePathSegment, buffer, contentType) {
const command = new PutObjectCommand({
Bucket: sails.config.custom.s3Bucket,
Key: filePathSegment,
Body: buffer,
ContentType: contentType,
});
await this.client.send(command);
}
async read(filePathSegment) {
const command = new GetObjectCommand({
Bucket: sails.config.custom.s3Bucket,
Key: filePathSegment,
});
const result = await this.client.send(command);
return result.Body;
}
async deleteDir(dirPathSegment) {
const listObjectsCommand = new ListObjectsV2Command({
Bucket: sails.config.custom.s3Bucket,
Prefix: dirPathSegment,
});
const result = await this.client.send(listObjectsCommand);
if (!result.Contents || result.Contents.length === 0) {
return;
}
const deleteObjectsCommand = new DeleteObjectsCommand({
Bucket: sails.config.custom.s3Bucket,
Delete: {
Objects: result.Contents.map(({ Key }) => ({ Key })),
},
});
await this.client.send(deleteObjectsCommand);
}
// eslint-disable-next-line class-methods-use-this
buildUrl(filePathSegment) {
return `${sails.hooks.s3.getBaseUrl()}/${filePathSegment}`;
}
}
module.exports = S3FileManager;

View File

@@ -0,0 +1,41 @@
const LocalFileManager = require('./LocalFileManager');
const S3FileManager = require('./S3FileManager');
/**
* file-manager hook
*
* @description :: A hook definition. Extends Sails by adding shadow routes, implicit actions,
* and/or initialization logic.
* @docs :: https://sailsjs.com/docs/concepts/extending-sails/hooks
*/
module.exports = function defineFileManagerHook(sails) {
let instance = null;
const createInstance = () => {
instance = sails.hooks.s3.isActive()
? new S3FileManager(sails.hooks.s3.getClient())
: new LocalFileManager();
};
return {
/**
* Runs when this Sails app loads/lifts.
*/
async initialize() {
sails.log.info('Initializing custom hook (`file-manager`)');
return new Promise((resolve) => {
sails.after('hook:s3:loaded', () => {
createInstance();
resolve();
});
});
},
getInstance() {
return instance;
},
};
};

View File

@@ -0,0 +1,64 @@
const { URL } = require('url');
const { S3Client } = require('@aws-sdk/client-s3');
/**
* s3 hook
*
* @description :: A hook definition. Extends Sails by adding shadow routes, implicit actions,
* and/or initialization logic.
* @docs :: https://sailsjs.com/docs/concepts/extending-sails/hooks
*/
module.exports = function defineS3Hook(sails) {
let client = null;
return {
/**
* Runs when this Sails app loads/lifts.
*/
async initialize() {
if (!sails.config.custom.s3Endpoint && !sails.config.custom.s3Region) {
return;
}
sails.log.info('Initializing custom hook (`s3`)');
client = new S3Client({
endpoint: sails.config.custom.s3Endpoint,
region: sails.config.custom.s3Region || '-',
credentials: {
accessKeyId: sails.config.custom.s3AccessKeyId,
secretAccessKey: sails.config.custom.s3SecretAccessKey,
},
forcePathStyle: sails.config.custom.s3ForcePathStyle,
});
},
getClient() {
return client;
},
getBaseUrl() {
if (sails.config.custom.s3Endpoint) {
const { protocol, host } = new URL(sails.config.custom.s3Endpoint);
if (sails.config.custom.s3ForcePathStyle) {
return `${protocol}//${host}/${sails.config.custom.s3Bucket}`;
}
return `${protocol}//${sails.config.custom.s3Bucket}.${host}`;
}
if (sails.config.custom.s3ForcePathStyle) {
return `https://s3.${sails.config.custom.s3Region}.amazonaws.com/${sails.config.custom.s3Bucket}`;
}
return `https://${sails.config.custom.s3Bucket}.s3.${sails.config.custom.s3Region}.amazonaws.com`;
},
isActive() {
return client !== null;
},
};
};

View File

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

View File

@@ -79,11 +79,13 @@ module.exports = {
}, },
customToJSON() { customToJSON() {
const fileManager = sails.hooks['file-manager'].getInstance();
return { return {
..._.omit(this, ['backgroundImage']), ..._.omit(this, ['backgroundImage']),
backgroundImage: this.backgroundImage && { backgroundImage: this.backgroundImage && {
url: `${sails.config.custom.projectBackgroundImagesUrl}/${this.backgroundImage.dirname}/original.${this.backgroundImage.extension}`, url: `${fileManager.buildUrl(`${sails.config.custom.projectBackgroundImagesPathSegment}/${this.backgroundImage.dirname}/original.${this.backgroundImage.extension}`)}`,
coverUrl: `${sails.config.custom.projectBackgroundImagesUrl}/${this.backgroundImage.dirname}/cover-336.${this.backgroundImage.extension}`, coverUrl: `${fileManager.buildUrl(`${sails.config.custom.projectBackgroundImagesPathSegment}/${this.backgroundImage.dirname}/cover-336.${this.backgroundImage.extension}`)}`,
}, },
}; };
}, },

View File

@@ -147,6 +147,7 @@ module.exports = {
tableName: 'user_account', tableName: 'user_account',
customToJSON() { customToJSON() {
const fileManager = sails.hooks['file-manager'].getInstance();
const isDefaultAdmin = this.email === sails.config.custom.defaultAdminEmail; const isDefaultAdmin = this.email === sails.config.custom.defaultAdminEmail;
return { return {
@@ -157,7 +158,7 @@ module.exports = {
isDeletionLocked: isDefaultAdmin, isDeletionLocked: isDefaultAdmin,
avatarUrl: avatarUrl:
this.avatar && this.avatar &&
`${sails.config.custom.userAvatarsUrl}/${this.avatar.dirname}/square-100.${this.avatar.extension}`, `${fileManager.buildUrl(`${sails.config.custom.userAvatarsPathSegment}/${this.avatar.dirname}/square-100.${this.avatar.extension}`)}`,
}; };
}, },
}; };

View File

@@ -8,11 +8,10 @@
* https://sailsjs.com/config/custom * https://sailsjs.com/config/custom
*/ */
const url = require('url'); const { URL } = require('url');
const path = require('path');
const sails = require('sails'); const sails = require('sails');
const parsedBasedUrl = new url.URL(process.env.BASE_URL); const parsedBasedUrl = new URL(process.env.BASE_URL);
module.exports.custom = { module.exports.custom = {
/** /**
@@ -28,24 +27,26 @@ module.exports.custom = {
tokenExpiresIn: parseInt(process.env.TOKEN_EXPIRES_IN, 10) || 365, tokenExpiresIn: parseInt(process.env.TOKEN_EXPIRES_IN, 10) || 365,
// Location to receive uploaded files in. Default (non-string value) is a Sails-specific location. // Location to receive uploaded files in. Default (non-string value) is a Sails-specific location.
fileUploadTmpDir: null, uploadsTempPath: null,
uploadsBasePath: sails.config.appPath,
userAvatarsPath: path.join(sails.config.paths.public, 'user-avatars'), userAvatarsPathSegment: 'public/user-avatars',
userAvatarsUrl: `${process.env.BASE_URL}/user-avatars`, projectBackgroundImagesPathSegment: 'public/project-background-images',
attachmentsPathSegment: 'private/attachments',
projectBackgroundImagesPath: path.join(sails.config.paths.public, 'project-background-images'),
projectBackgroundImagesUrl: `${process.env.BASE_URL}/project-background-images`,
attachmentsPath: path.join(sails.config.appPath, 'private', 'attachments'),
attachmentsUrl: `${process.env.BASE_URL}/attachments`,
defaultAdminEmail: defaultAdminEmail:
process.env.DEFAULT_ADMIN_EMAIL && process.env.DEFAULT_ADMIN_EMAIL.toLowerCase(), process.env.DEFAULT_ADMIN_EMAIL && process.env.DEFAULT_ADMIN_EMAIL.toLowerCase(),
showDetailedAuthErrors: process.env.SHOW_DETAILED_AUTH_ERRORS === 'true', showDetailedAuthErrors: process.env.SHOW_DETAILED_AUTH_ERRORS === 'true',
allowAllToCreateProjects: process.env.ALLOW_ALL_TO_CREATE_PROJECTS === 'true', allowAllToCreateProjects: process.env.ALLOW_ALL_TO_CREATE_PROJECTS === 'true',
s3Endpoint: process.env.S3_ENDPOINT,
s3Region: process.env.S3_REGION,
s3AccessKeyId: process.env.S3_ACCESS_KEY_ID,
s3SecretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
s3Bucket: process.env.S3_BUCKET,
s3ForcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true',
oidcIssuer: process.env.OIDC_ISSUER, oidcIssuer: process.env.OIDC_ISSUER,
oidcClientId: process.env.OIDC_CLIENT_ID, oidcClientId: process.env.OIDC_CLIENT_ID,
oidcClientSecret: process.env.OIDC_CLIENT_SECRET, oidcClientSecret: process.env.OIDC_CLIENT_SECRET,
@@ -82,5 +83,10 @@ module.exports.custom = {
slackBotToken: process.env.SLACK_BOT_TOKEN, slackBotToken: process.env.SLACK_BOT_TOKEN,
slackChannelId: process.env.SLACK_CHANNEL_ID, slackChannelId: process.env.SLACK_CHANNEL_ID,
googleChatWebhookUrl: process.env.GOOGLE_CHAT_WEBHOOK_URL, googleChatWebhookUrl: process.env.GOOGLE_CHAT_WEBHOOK_URL,
telegramBotToken: process.env.TELEGRAM_BOT_TOKEN,
telegramChatId: process.env.TELEGRAM_CHAT_ID,
telegramThreadId: process.env.TELEGRAM_THREAD_ID,
}; };

View File

@@ -19,11 +19,11 @@
* https://sailsjs.com/docs/concepts/deployment * https://sailsjs.com/docs/concepts/deployment
*/ */
const url = require('url'); const { URL } = require('url');
const { customLogger } = require('../../utils/logger'); const { customLogger } = require('../../utils/logger');
const parsedBasedUrl = new url.URL(process.env.BASE_URL); const parsedBasedUrl = new URL(process.env.BASE_URL);
module.exports = { module.exports = {
/** /**

View File

@@ -135,13 +135,21 @@ module.exports.routes = {
'PATCH /api/notifications/:ids': 'notifications/update', 'PATCH /api/notifications/:ids': 'notifications/update',
'GET /user-avatars/*': { 'GET /user-avatars/*': {
fn: staticDirServer('/user-avatars', () => path.resolve(sails.config.custom.userAvatarsPath)), fn: staticDirServer('/user-avatars', () =>
path.join(
path.resolve(sails.config.custom.uploadsBasePath),
sails.config.custom.userAvatarsPathSegment,
),
),
skipAssets: false, skipAssets: false,
}, },
'GET /project-background-images/*': { 'GET /project-background-images/*': {
fn: staticDirServer('/project-background-images', () => fn: staticDirServer('/project-background-images', () =>
path.resolve(sails.config.custom.projectBackgroundImagesPath), path.join(
path.resolve(sails.config.custom.uploadsBasePath),
sails.config.custom.projectBackgroundImagesPathSegment,
),
), ),
skipAssets: false, skipAssets: false,
}, },

1860
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,14 +27,15 @@
} }
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.688.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"dotenv-cli": "^7.4.2", "dotenv-cli": "^7.4.2",
"fs-extra": "^11.2.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"knex": "^3.1.0", "knex": "^3.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.30.1", "moment": "^2.30.1",
"move-file": "^2.1.0",
"nodemailer": "^6.9.15", "nodemailer": "^6.9.15",
"openid-client": "^5.7.0", "openid-client": "^5.7.0",
"rimraf": "^5.0.10", "rimraf": "^5.0.10",