Merge branch 'master' into notification-settings-frontend

This commit is contained in:
Max 2025-04-07 16:55:56 +02:00 committed by Ulf Gebhardt
commit 0f25d124d4
Signed by: ulfgebhardt
GPG Key ID: DA6B843E748679C9
82 changed files with 1140 additions and 368 deletions

View File

@ -68,7 +68,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@70b2cdc6480c1a8b86edf1777157f8f437de2166
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |

209
backend/.eslintrc.cjs Normal file
View File

@ -0,0 +1,209 @@
// eslint-disable-next-line import/no-commonjs
module.exports = {
root: true,
env: {
node: true,
},
parser: '@typescript-eslint/parser',
plugins: ['prettier', '@typescript-eslint', 'import', 'n', 'promise', 'security', 'no-catch-all',],
extends: [
'standard',
'eslint:recommended',
'plugin:prettier/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'plugin:security/recommended-legacy',
'plugin:@eslint-community/eslint-comments/recommended',
],
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: {
project: ['./tsconfig.json'],
},
node: true,
},
},
rules: {
'no-catch-all/no-catch-all': 'error',
'no-console': 'error',
camelcase: 'error',
'no-debugger': 'error',
'prettier/prettier': [
'error',
{
htmlWhitespaceSensitivity: 'ignore',
},
],
// import
'import/export': 'error',
// 'import/no-deprecated': 'error',
'import/no-empty-named-blocks': 'error',
// 'import/no-extraneous-dependencies': 'error',
'import/no-mutable-exports': 'error',
'import/no-unused-modules': 'error',
'import/no-named-as-default': 'error',
'import/no-named-as-default-member': 'error',
'import/no-amd': 'error',
'import/no-commonjs': 'error',
'import/no-import-module-exports': 'error',
'import/no-nodejs-modules': 'off',
'import/unambiguous': 'off', // not compatible with scriptless vue files
'import/default': 'error',
// 'import/named': 'error',
'import/namespace': 'error',
'import/no-absolute-path': 'error',
'import/no-cycle': 'error',
'import/no-dynamic-require': 'error',
'import/no-internal-modules': 'off',
'import/no-relative-packages': 'error',
// 'import/no-relative-parent-imports': ['error', { ignore: ['@/*'] }],
'import/no-self-import': 'error',
'import/no-unresolved': 'error',
'import/no-useless-path-segments': 'error',
'import/no-webpack-loader-syntax': 'error',
'import/consistent-type-specifier-style': 'error',
'import/exports-last': 'off',
'import/extensions': 'error',
'import/first': 'error',
'import/group-exports': 'off',
'import/newline-after-import': 'error',
// 'import/no-anonymous-default-export': 'error',
// 'import/no-default-export': 'error',
'import/no-duplicates': 'error',
'import/no-named-default': 'error',
'import/no-namespace': 'error',
'import/no-unassigned-import': 'error',
// 'import/order': [
// 'error',
// {
// groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
// 'newlines-between': 'always',
// pathGroups: [
// {
// pattern: '@?*/**',
// group: 'external',
// position: 'after',
// },
// {
// pattern: '@/**',
// group: 'external',
// position: 'after',
// },
// ],
// alphabetize: {
// order: 'asc' /* sort in ascending order. Options: ['ignore', 'asc', 'desc'] */,
// caseInsensitive: true /* ignore case. Options: [true, false] */,
// },
// distinctGroup: true,
// },
// ],
'import/prefer-default-export': 'off',
// n
'n/handle-callback-err': 'error',
'n/no-callback-literal': 'error',
'n/no-exports-assign': 'error',
// 'n/no-extraneous-import': 'error',
'n/no-extraneous-require': 'error',
'n/no-hide-core-modules': 'error',
'n/no-missing-import': 'off', // not compatible with typescript
'n/no-missing-require': 'error',
'n/no-new-require': 'error',
'n/no-path-concat': 'error',
'n/no-process-exit': 'error',
'n/no-unpublished-bin': 'error',
'n/no-unpublished-import': 'off', // TODO need to exclude seeds
'n/no-unpublished-require': 'error',
'n/no-unsupported-features': ['error', { ignores: ['modules'] }],
'n/no-unsupported-features/es-builtins': 'error',
'n/no-unsupported-features/es-syntax': 'error',
'n/no-unsupported-features/node-builtins': 'error',
'n/process-exit-as-throw': 'error',
'n/shebang': 'error',
//'n/callback-return': 'error',
'n/exports-style': 'error',
'n/file-extension-in-import': 'off',
'n/global-require': 'error',
'n/no-mixed-requires': 'error',
'n/no-process-env': 'error',
'n/no-restricted-import': 'error',
'n/no-restricted-require': 'error',
// 'n/no-sync': 'error',
'n/prefer-global/buffer': 'error',
'n/prefer-global/console': 'error',
'n/prefer-global/process': 'error',
'n/prefer-global/text-decoder': 'error',
'n/prefer-global/text-encoder': 'error',
'n/prefer-global/url': 'error',
'n/prefer-global/url-search-params': 'error',
'n/prefer-promises/dns': 'error',
'n/prefer-promises/fs': 'error',
// promise
'promise/catch-or-return': 'error',
'promise/no-return-wrap': 'error',
'promise/param-names': 'error',
'promise/always-return': 'error',
'promise/no-native': 'off',
'promise/no-nesting': 'warn',
'promise/no-promise-in-callback': 'warn',
'promise/no-callback-in-promise': 'warn',
'promise/avoid-new': 'warn',
'promise/no-new-statics': 'error',
'promise/no-return-in-finally': 'warn',
'promise/valid-params': 'warn',
'promise/prefer-await-to-callbacks': 'error',
'promise/no-multiple-resolved': 'error',
// eslint comments
'@eslint-community/eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }],
'@eslint-community/eslint-comments/no-restricted-disable': 'error',
'@eslint-community/eslint-comments/no-use': 'off',
'@eslint-community/eslint-comments/require-description': 'off',
},
overrides: [
// only for ts files
{
files: ['*.ts', '*.tsx'],
extends: [
// 'plugin:@typescript-eslint/recommended',
// 'plugin:@typescript-eslint/recommended-requiring-type-checking',
// 'plugin:@typescript-eslint/strict',
],
rules: {
// allow explicitly defined dangling promises
// '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }],
'no-void': ['error', { allowAsStatement: true }],
// ignore prefer-regexp-exec rule to allow string.match(regex)
'@typescript-eslint/prefer-regexp-exec': 'off',
// this should not run on ts files: https://github.com/import-js/eslint-plugin-import/issues/2215#issuecomment-911245486
'import/unambiguous': 'off',
// this is not compatible with typeorm, due to joined tables can be null, but are not defined as nullable
'@typescript-eslint/no-unnecessary-condition': 'off',
},
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
// this is to properly reference the referenced project database without requirement of compiling it
// eslint-disable-next-line camelcase
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
},
},
{
files: ['*.spec.ts'],
plugins: ['jest'],
env: {
jest: true,
},
rules: {
'jest/no-disabled-tests': 'error',
'jest/no-focused-tests': 'error',
'jest/no-identical-title': 'error',
'jest/prefer-to-have-length': 'error',
'jest/valid-expect': 'error',
'@typescript-eslint/unbound-method': 'off',
'jest/unbound-method': 'error',
},
},
],
};

View File

@ -1,219 +0,0 @@
module.exports = {
root: true,
env: {
// es6: true,
node: true,
},
/* parserOptions: {
parser: 'babel-eslint'
},*/
parser: '@typescript-eslint/parser',
plugins: ['prettier', '@typescript-eslint' /*, 'import', 'n', 'promise'*/],
extends: [
'standard',
// 'eslint:recommended',
'plugin:prettier/recommended',
// 'plugin:import/recommended',
// 'plugin:import/typescript',
// 'plugin:security/recommended',
// 'plugin:@eslint-community/eslint-comments/recommended',
],
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: {
project: ['./tsconfig.json'],
},
node: true,
},
},
/* rules: {
//'indent': [ 'error', 2 ],
//'quotes': [ "error", "single"],
// 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
> 'no-console': ['error'],
> 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
> 'prettier/prettier': ['error'],
}, */
rules: {
'no-console': 'error',
camelcase: 'error',
'no-debugger': 'error',
'prettier/prettier': [
'error',
{
htmlWhitespaceSensitivity: 'ignore',
},
],
// import
// 'import/export': 'error',
// 'import/no-deprecated': 'error',
// 'import/no-empty-named-blocks': 'error',
// 'import/no-extraneous-dependencies': 'error',
// 'import/no-mutable-exports': 'error',
// 'import/no-unused-modules': 'error',
// 'import/no-named-as-default': 'error',
// 'import/no-named-as-default-member': 'error',
// 'import/no-amd': 'error',
// 'import/no-commonjs': 'error',
// 'import/no-import-module-exports': 'error',
// 'import/no-nodejs-modules': 'off',
// 'import/unambiguous': 'error',
// 'import/default': 'error',
// 'import/named': 'error',
// 'import/namespace': 'error',
// 'import/no-absolute-path': 'error',
// 'import/no-cycle': 'error',
// 'import/no-dynamic-require': 'error',
// 'import/no-internal-modules': 'off',
// 'import/no-relative-packages': 'error',
// 'import/no-relative-parent-imports': ['error', { ignore: ['@/*'] }],
// 'import/no-self-import': 'error',
// 'import/no-unresolved': 'error',
// 'import/no-useless-path-segments': 'error',
// 'import/no-webpack-loader-syntax': 'error',
// 'import/consistent-type-specifier-style': 'error',
// 'import/exports-last': 'off',
// 'import/extensions': 'error',
// 'import/first': 'error',
// 'import/group-exports': 'off',
// 'import/newline-after-import': 'error',
// 'import/no-anonymous-default-export': 'error',
// 'import/no-default-export': 'error',
// 'import/no-duplicates': 'error',
// 'import/no-named-default': 'error',
// 'import/no-namespace': 'error',
// 'import/no-unassigned-import': 'error',
// 'import/order': [
// 'error',
// {
// groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
// 'newlines-between': 'always',
// pathGroups: [
// {
// pattern: '@?*/**',
// group: 'external',
// position: 'after',
// },
// {
// pattern: '@/**',
// group: 'external',
// position: 'after',
// },
// ],
// alphabetize: {
// order: 'asc' /* sort in ascending order. Options: ['ignore', 'asc', 'desc'] */,
// caseInsensitive: true /* ignore case. Options: [true, false] */,
// },
// distinctGroup: true,
// },
// ],
// 'import/prefer-default-export': 'off',
// n
// 'n/handle-callback-err': 'error',
// 'n/no-callback-literal': 'error',
// 'n/no-exports-assign': 'error',
// 'n/no-extraneous-import': 'error',
// 'n/no-extraneous-require': 'error',
// 'n/no-hide-core-modules': 'error',
// 'n/no-missing-import': 'off', // not compatible with typescript
// 'n/no-missing-require': 'error',
// 'n/no-new-require': 'error',
// 'n/no-path-concat': 'error',
// 'n/no-process-exit': 'error',
// 'n/no-unpublished-bin': 'error',
// 'n/no-unpublished-import': 'off', // TODO need to exclude seeds
// 'n/no-unpublished-require': 'error',
// 'n/no-unsupported-features': ['error', { ignores: ['modules'] }],
// 'n/no-unsupported-features/es-builtins': 'error',
// 'n/no-unsupported-features/es-syntax': 'error',
// 'n/no-unsupported-features/node-builtins': 'error',
// 'n/process-exit-as-throw': 'error',
// 'n/shebang': 'error',
// 'n/callback-return': 'error',
// 'n/exports-style': 'error',
// 'n/file-extension-in-import': 'off',
// 'n/global-require': 'error',
// 'n/no-mixed-requires': 'error',
// 'n/no-process-env': 'error',
// 'n/no-restricted-import': 'error',
// 'n/no-restricted-require': 'error',
// 'n/no-sync': 'error',
// 'n/prefer-global/buffer': 'error',
// 'n/prefer-global/console': 'error',
// 'n/prefer-global/process': 'error',
// 'n/prefer-global/text-decoder': 'error',
// 'n/prefer-global/text-encoder': 'error',
// 'n/prefer-global/url': 'error',
// 'n/prefer-global/url-search-params': 'error',
// 'n/prefer-promises/dns': 'error',
// 'n/prefer-promises/fs': 'error',
// promise
// 'promise/catch-or-return': 'error',
// 'promise/no-return-wrap': 'error',
// 'promise/param-names': 'error',
// 'promise/always-return': 'error',
// 'promise/no-native': 'off',
// 'promise/no-nesting': 'warn',
// 'promise/no-promise-in-callback': 'warn',
// 'promise/no-callback-in-promise': 'warn',
// 'promise/avoid-new': 'warn',
// 'promise/no-new-statics': 'error',
// 'promise/no-return-in-finally': 'warn',
// 'promise/valid-params': 'warn',
// 'promise/prefer-await-to-callbacks': 'error',
// 'promise/no-multiple-resolved': 'error',
// eslint comments
// '@eslint-community/eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }],
// '@eslint-community/eslint-comments/no-restricted-disable': 'error',
// '@eslint-community/eslint-comments/no-use': 'off',
// '@eslint-community/eslint-comments/require-description': 'off',
},
overrides: [
// only for ts files
{
files: ['*.ts', '*.tsx'],
extends: [
// 'plugin:@typescript-eslint/recommended',
// 'plugin:@typescript-eslint/recommended-requiring-type-checking',
// 'plugin:@typescript-eslint/strict',
],
rules: {
// allow explicitly defined dangling promises
// '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }],
'no-void': ['error', { allowAsStatement: true }],
// ignore prefer-regexp-exec rule to allow string.match(regex)
'@typescript-eslint/prefer-regexp-exec': 'off',
// this should not run on ts files: https://github.com/import-js/eslint-plugin-import/issues/2215#issuecomment-911245486
'import/unambiguous': 'off',
// this is not compatible with typeorm, due to joined tables can be null, but are not defined as nullable
'@typescript-eslint/no-unnecessary-condition': 'off',
},
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
// this is to properly reference the referenced project database without requirement of compiling it
// eslint-disable-next-line camelcase
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
},
},
{
files: ['*.spec.ts'],
plugins: ['jest'],
env: {
jest: true,
},
rules: {
'jest/no-disabled-tests': 'error',
'jest/no-focused-tests': 'error',
'jest/no-identical-title': 'error',
'jest/prefer-to-have-length': 'error',
'jest/valid-expect': 'error',
'@typescript-eslint/unbound-method': 'off',
// 'jest/unbound-method': 'error',
},
},
],
};

View File

@ -76,7 +76,7 @@
"metascraper-video": "^5.46.11",
"metascraper-youtube": "^5.46.11",
"migrate": "^2.1.0",
"mime-types": "^2.1.35",
"mime-types": "^3.0.1",
"minimatch": "^9.0.4",
"mustache": "^4.2.0",
"neo4j-driver": "^4.4.11",
@ -95,6 +95,7 @@
"xregexp": "^5.1.2"
},
"devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "^4.4.1",
"@faker-js/faker": "9.6.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.14.0",
@ -108,6 +109,7 @@
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.11.0",
"eslint-plugin-n": "^16.6.2",
"eslint-plugin-no-catch-all": "^1.1.0",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-security": "^3.0.1",
@ -121,7 +123,9 @@
},
"resolutions": {
"**/**/fs-capacitor": "^6.2.0",
"**/graphql-upload": "^11.0.0",
"nan": "2.17.0"
"**/graphql-upload": "^11.0.0"
},
"engines": {
"node": ">=20.12.1"
}
}

View File

@ -1,11 +1,14 @@
import dotenv from 'dotenv'
/* eslint-disable n/no-process-env */
/* eslint-disable n/no-unpublished-require */
/* eslint-disable n/no-missing-require */
import { config } from 'dotenv'
import emails from './emails'
import metadata from './metadata'
// Load env file
if (require.resolve) {
try {
dotenv.config({ path: require.resolve('../../.env') })
config({ path: require.resolve('../../.env') })
} catch (error) {
// This error is thrown when the .env is not found
if (error.code !== 'MODULE_NOT_FOUND') {

View File

@ -1,5 +1,6 @@
/* eslint-disable n/no-process-exit */
import CONFIG from '../config'
import { cleanDatabase } from '../db/factories'
import { cleanDatabase } from './factories'
if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) {
throw new Error(`You cannot clean the database in a non-staging and real production environment!`)
@ -10,6 +11,7 @@ if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) {
await cleanDatabase()
console.log('Successfully deleted all nodes and relations!') // eslint-disable-line no-console
process.exit(0)
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (err) {
console.log(`Error occurred deleting the nodes and relations (reset the db)\n\n${err}`) // eslint-disable-line no-console
process.exit(1)

View File

@ -1,2 +1,5 @@
/* eslint-disable import/no-commonjs */
// eslint-disable-next-line n/no-unpublished-require
const tsNode = require('ts-node')
module.exports = tsNode.register

View File

@ -1,4 +1,4 @@
import { getDriver, getNeode } from '../../db/neo4j'
import { getDriver, getNeode } from '../neo4j'
import { hashSync } from 'bcryptjs'
import { v4 as uuid } from 'uuid'
import { categories } from '../../constants/categories'
@ -30,6 +30,7 @@ const createCategories = async (session) => {
try {
await createCategoriesTxResultPromise
console.log('Successfully created categories!') // eslint-disable-line no-console
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
console.log(`Error creating categories: ${error}`) // eslint-disable-line no-console
}
@ -44,6 +45,7 @@ const createDefaultAdminUser = async (session) => {
try {
const userCount = parseInt(String(await readTxResultPromise))
if (userCount === 0) createAdmin = true
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
console.log(error) // eslint-disable-line no-console
}
@ -70,6 +72,7 @@ const createDefaultAdminUser = async (session) => {
try {
await createAdminTxResultPromise
console.log('Successfully created default admin user!') // eslint-disable-line no-console
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
console.log(error) // eslint-disable-line no-console
}
@ -92,6 +95,7 @@ class Store {
// eslint-disable-next-line no-console
console.log('Successfully created database indices and constraints!')
next()
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
console.log(error) // eslint-disable-line no-console
next(error, null)
@ -121,6 +125,7 @@ class Store {
}
const [{ title: lastRun }] = migrations
next(null, { lastRun, migrations })
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
console.log(error) // eslint-disable-line no-console
next(error)
@ -156,6 +161,7 @@ class Store {
try {
await writeTxResultPromise
next()
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
console.log(error) // eslint-disable-line no-console
next(error)
@ -165,4 +171,4 @@ class Store {
}
}
module.exports = Store
export default Store

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '../neo4j'
export const description = ''

View File

@ -1,7 +1,8 @@
/* eslint-disable promise/prefer-await-to-callbacks */
import { throwError, concat } from 'rxjs'
import { flatMap, mergeMap, map, catchError, filter } from 'rxjs/operators'
import { getDriver } from '../neo4j'
import normalizeEmail from '../../schema/resolvers//helpers/normalizeEmail'
import normalizeEmail from '../../schema/resolvers/helpers/normalizeEmail'
export const description = `
This migration merges duplicate :User and :EmailAddress nodes. It became

View File

@ -1,3 +1,4 @@
/* eslint-disable promise/prefer-await-to-callbacks */
import { throwError, concat } from 'rxjs'
import { flatMap, mergeMap, map, catchError } from 'rxjs/operators'
import { getDriver } from '../neo4j'

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '../neo4j'
export const description = `
This migration creates a MUTED relationship between two edges(:User) that have a pre-existing BLOCKED relationship.
@ -21,6 +21,7 @@ export async function up(next) {
`,
)
await transaction.commit()
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
// eslint-disable-next-line no-console
console.log(error)
@ -38,6 +39,7 @@ export function down(next) {
try {
// Rollback your migration here.
next()
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (err) {
next(err)
} finally {

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '../neo4j'
export const description = `
This migration swaps the value stored in Location.lat with the value

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '../neo4j'
export const description =
'This migration adds a fulltext index for the tags in order to search for Hasthags.'

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '../neo4j'
export const description = `
We introduced a new node label 'Image' and we need a primary key for it. Best
@ -48,6 +48,7 @@ export async function down(next) {
`)
await transaction.commit()
next()
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
// eslint-disable-next-line no-console
console.log(error)

View File

@ -1,4 +1,5 @@
import { getDriver } from '../../db/neo4j'
/* eslint-disable security/detect-non-literal-fs-filename */
import { getDriver } from '../neo4j'
import { existsSync, createReadStream } from 'fs'
import path from 'path'
import { S3 } from 'aws-sdk'
@ -95,6 +96,7 @@ export async function down(next) {
await transaction.run(``)
await transaction.commit()
next()
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
// eslint-disable-next-line no-console
console.log(error)

View File

@ -1,5 +1,5 @@
/* eslint-disable no-console */
import { getDriver } from '../../db/neo4j'
import { getDriver } from '../neo4j'
export const description = `
Refactor all our image properties on posts and users to a dedicated type

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '../neo4j'
export const description =
'We should not maintain obsolete attributes for users who have been deleted.'

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '../neo4j'
export const description =
'We should not maintain obsolete attributes for posts which have been deleted.'

View File

@ -1,4 +1,5 @@
import { getDriver } from '../../db/neo4j'
/* eslint-disable security/detect-non-literal-fs-filename */
import { getDriver } from '../neo4j'
import { existsSync } from 'fs'
export const description = `

View File

@ -1,9 +1,9 @@
'use strict'
module.exports.up = function (next) {
export async function up(next) {
next()
}
module.exports.down = function (next) {
export async function down(next) {
next()
}

View File

@ -1,10 +1,10 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '../neo4j'
export const description = `
This migration adds the clickedCount property to all posts, setting it to 0.
`
module.exports.up = async function (next) {
export async function up(next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()
@ -28,7 +28,7 @@ module.exports.up = async function (next) {
}
}
module.exports.down = async function (next) {
export async function down(next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()

View File

@ -1,10 +1,10 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '../neo4j'
export const description = `
This migration adds the viewedTeaserCount property to all posts, setting it to 0.
`
module.exports.up = async function (next) {
export async function up(next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()
@ -28,7 +28,7 @@ module.exports.up = async function (next) {
}
}
module.exports.down = async function (next) {
export async function down(next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '../neo4j'
import { v4 as uuid } from 'uuid'
export const description =

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '../neo4j'
export const description = ''

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '../neo4j'
export const description = `
We introduced a new node label 'Group' and we need two primary keys 'id' and 'slug' for it.

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '../neo4j'
export const description = ''

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '../neo4j'
export const description = 'Add to all existing posts the Article label'

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '../neo4j'
export const description = 'Add postType property Article to all posts'

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '../neo4j'
export const description = `
Transform event start and end date of format 'YYYY-MM-DD HH:MM:SS' in CEST

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '../neo4j'
export const description = `
All authors observe their posts.

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '../neo4j'
export const description =
'Transforms the `sendNotificationEmails` property on User to a multi value system'
@ -14,6 +14,7 @@ export async function up(next) {
MATCH (user:User)
SET user.emailNotificationsCommentOnObservedPost = user.sendNotificationEmails
SET user.emailNotificationsMention = user.sendNotificationEmails
SET user.emailNotificationsChatMessage = user.sendNotificationEmails
SET user.emailNotificationsGroupMemberJoined = user.sendNotificationEmails
SET user.emailNotificationsGroupMemberLeft = user.sendNotificationEmails
SET user.emailNotificationsGroupMemberRemoved = user.sendNotificationEmails
@ -46,6 +47,7 @@ export async function down(next) {
SET user.sendNotificationEmails = true
REMOVE user.emailNotificationsCommentOnObservedPost
REMOVE user.emailNotificationsMention
REMOVE user.emailNotificationsChatMessage
REMOVE user.emailNotificationsGroupMemberJoined
REMOVE user.emailNotificationsGroupMemberLeft
REMOVE user.emailNotificationsGroupMemberRemoved

View File

@ -1,5 +1,6 @@
/* eslint-disable import/no-named-as-default-member */
import neo4j from 'neo4j-driver'
import CONFIG from './../config'
import CONFIG from '../config'
import Neode from 'neode'
import models from '../models'

View File

@ -1,10 +1,11 @@
/* eslint-disable n/no-process-exit */
import sample from 'lodash/sample'
import { createTestClient } from 'apollo-server-testing'
import CONFIG from '../config'
import createServer from '../server'
import { faker } from '@faker-js/faker'
import Factory from '../db/factories'
import { getNeode, getDriver } from '../db/neo4j'
import Factory from './factories'
import { getNeode, getDriver } from './neo4j'
import {
createGroupMutation,
joinGroupMutation,
@ -1565,6 +1566,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
await driver.close()
await neode.close()
process.exit(0)
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (err) {
/* eslint-disable-next-line no-console */
console.error(err)

View File

@ -1,3 +1,5 @@
/* eslint-disable promise/prefer-await-to-callbacks */
/* eslint-disable security/detect-object-injection */
/**
* Provide a way to iterate for each element in an array while waiting for async functions to finish
*

View File

@ -1,3 +1,4 @@
/* eslint-disable promise/avoid-new */
// sometime we have to wait to check a db state by having a look into the db in a certain moment
// or we wait a bit to check if we missed to set an await somewhere
// see: https://www.sitepoint.com/delay-sleep-pause-wait/

View File

@ -1,3 +1,5 @@
/* eslint-disable promise/prefer-await-to-callbacks */
/* eslint-disable security/detect-object-injection */
/**
* iterate through all fields and replace it with the callback result
* @property data Array

View File

@ -1,5 +1,5 @@
import jwt from 'jsonwebtoken'
import CONFIG from './../config'
import CONFIG from '../config'
export default async (driver, authorizationHeader) => {
if (!authorizationHeader) return null
@ -8,6 +8,7 @@ export default async (driver, authorizationHeader) => {
try {
const decoded = await jwt.verify(token, CONFIG.JWT_SECRET)
id = decoded.sub
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (err) {
return null
}

View File

@ -1,6 +1,6 @@
import encode from './encode'
import jwt from 'jsonwebtoken'
import CONFIG from './../config'
import CONFIG from '../config'
describe('encode', () => {
let payload

View File

@ -1,5 +1,5 @@
import jwt from 'jsonwebtoken'
import CONFIG from './../config'
import CONFIG from '../config'
// Generate an Access Token for the given User ID
export default function encode(user) {

View File

@ -1,4 +1,5 @@
import * as cheerio from 'cheerio'
import { load } from 'cheerio'
// eslint-disable-next-line import/extensions
import { exec, build } from 'xregexp/xregexp-all.js'
// formats of a Hashtag:
// https://en.wikipedia.org/w/index.php?title=Hashtag&oldid=905141980#Style
@ -10,7 +11,7 @@ const regX = build('^((\\pL+[\\pL0-9]*)|([0-9]+\\pL+[\\pL0-9]*))$')
export default function (content?) {
if (!content) return []
const $ = cheerio.load(content)
const $ = load(content)
// We can not search for class '.hashtag', because the classes are removed at the 'xss' middleware.
// But we have to know, which Hashtags are removed from the content as well, so we search for the 'a' html-tag.
const ids = $('a[data-hashtag-id]')

View File

@ -1,4 +1,4 @@
import extractHashtags from '../hashtags/extractHashtags'
import extractHashtags from './extractHashtags'
const updateHashtagsOfPost = async (postId, hashtags, context) => {
if (!hashtags.length) return

View File

@ -1,3 +1,4 @@
/* eslint-disable security/detect-unsafe-regex */
import sanitizeHtml from 'sanitize-html'
import linkifyHtml from 'linkify-html'

View File

@ -1,5 +1,5 @@
import CONFIG from '../../../config'
import { cleanHtml } from '../../../middleware/helpers/cleanHtml'
import { cleanHtml } from '../cleanHtml'
import nodemailer from 'nodemailer'
import { htmlToText } from 'nodemailer-html-to-text'

View File

@ -6,6 +6,7 @@ import {
resetPasswordTemplate,
wrongAccountTemplate,
notificationTemplate,
chatMessageTemplate,
} from './templateBuilder'
const englishHint = 'English version below!'
@ -34,6 +35,12 @@ const resetPasswordTemplateData = () => ({
name: 'Mr Example',
},
})
const chatMessageTemplateData = {
email: 'test@example.org',
variables: {
name: 'Mr Example',
},
}
const wrongAccountTemplateData = () => ({
email: 'test@example.org',
variables: {},
@ -163,6 +170,31 @@ describe('templateBuilder', () => {
})
})
describe('chatMessageTemplate', () => {
describe('multi language', () => {
it('e-mail is build with all data', () => {
const subject = 'Neue Chatnachricht | New chat message'
const actionUrl = new URL('/chat', CONFIG.CLIENT_URI).toString()
const enContent = 'You have received a new chat message.'
const deContent = 'Du hast eine neue Chatnachricht erhalten.'
testEmailData(null, chatMessageTemplate, chatMessageTemplateData, [
...textsStandard,
{
templPropName: 'subject',
isContaining: false,
text: subject,
},
englishHint,
actionUrl,
chatMessageTemplateData.variables.name,
enContent,
deContent,
supportUrl,
])
})
})
})
describe('wrongAccountTemplate', () => {
describe('multi language', () => {
it('e-mail is build with all data', () => {

View File

@ -1,3 +1,4 @@
/* eslint-disable import/no-namespace */
import mustache from 'mustache'
import CONFIG from '../../../config'
import metadata from '../../../config/metadata'
@ -71,6 +72,19 @@ export const resetPasswordTemplate = ({ email, variables: { nonce, name } }) =>
}
}
export const chatMessageTemplate = ({ email, variables: { name } }) => {
const subject = 'Neue Chatnachricht | New chat message'
const actionUrl = new URL('/chat', CONFIG.CLIENT_URI)
const renderParams = { ...defaultParams, englishHint, actionUrl, name, subject }
return {
from,
to: email,
subject,
html: mustache.render(templates.layout, renderParams, { content: templates.chatMessage }),
}
}
export const wrongAccountTemplate = ({ email, _variables = {} }) => {
const subject = 'Falsche Mailadresse? | Wrong E-mail?'
const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI)

View File

@ -0,0 +1,105 @@
<!-- Email Body German : BEGIN -->
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<!-- Hero Image, Flush : BEGIN -->
<tr>
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>
<!-- Hero Image, Flush : END -->
<!-- 1 Column Text + Button : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Hallo {{ name }}!</h1>
<p style="margin: 0;">Du hast eine neue Chatnachricht erhalten.</p>
</td>
</tr>
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Chat anzeigen</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text + Button : END -->
</table>
<!-- Email Body German : END -->
<!-- Email Body English : BEGIN -->
<table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<tr>
<td style="padding: 20px 0; text-align: center">
</td>
</tr>
<!-- Hero Image, Flush : BEGIN -->
<tr>
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>
<!-- Hero Image, Flush : END -->
<!-- 1 Column Text + Button : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Hello {{ name }}!</h1>
<p style="margin: 0;">You have received a new chat message.</p>
</td>
</tr>
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Show Chat</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text + Button : END -->
</table>
<!-- Email Body English : END -->

View File

@ -1,3 +1,4 @@
/* eslint-disable security/detect-non-literal-fs-filename */
import fs from 'fs'
import path from 'path'

View File

@ -1,3 +1,4 @@
/* eslint-disable security/detect-non-literal-fs-filename */
import fs from 'fs'
import path from 'path'

View File

@ -1,3 +1,4 @@
/* eslint-disable security/detect-non-literal-fs-filename */
import fs from 'fs'
import path from 'path'
@ -7,5 +8,6 @@ export const signup = readFile('./signup.html')
export const passwordReset = readFile('./resetPassword.html')
export const wrongAccount = readFile('./wrongAccount.html')
export const emailVerification = readFile('./emailVerification.html')
export const chatMessage = readFile('./chatMessage.html')
export const layout = readFile('./layout.html')

View File

@ -0,0 +1,46 @@
import { isUserOnline } from './isUserOnline'
let user
describe('isUserOnline', () => {
beforeEach(() => {
user = {
properties: {
lastActiveAt: null,
awaySince: null,
lastOnlineStatus: null,
},
}
})
describe('user has lastOnlineStatus `online`', () => {
it('returns true if he was active within the last 90 seconds', () => {
user.properties.lastOnlineStatus = 'online'
user.properties.lastActiveAt = new Date()
expect(isUserOnline(user)).toBe(true)
})
it('returns false if he was not active within the last 90 seconds', () => {
user.properties.lastOnlineStatus = 'online'
user.properties.lastActiveAt = new Date().getTime() - 90001
expect(isUserOnline(user)).toBe(false)
})
})
describe('user has lastOnlineStatus `away`', () => {
it('returns true if he went away less then 180 seconds ago', () => {
user.properties.lastOnlineStatus = 'away'
user.properties.awaySince = new Date()
expect(isUserOnline(user)).toBe(true)
})
it('returns false if he went away more then 180 seconds ago', () => {
user.properties.lastOnlineStatus = 'away'
user.properties.awaySince = new Date().getTime() - 180001
expect(isUserOnline(user)).toBe(false)
})
})
describe('user is freshly created and has never logged in', () => {
it('returns false', () => {
expect(isUserOnline(user)).toBe(false)
})
})
})

View File

@ -0,0 +1,16 @@
export const isUserOnline = (user) => {
// Is Recipient considered online
const lastActive = new Date(user.properties.lastActiveAt).getTime()
const awaySince = new Date(user.properties.awaySince).getTime()
const now = new Date().getTime()
const status = user.properties.lastOnlineStatus
if (
// online & last active less than 1.5min -> online
(status === 'online' && now - lastActive < 90000) ||
// away for less then 3min -> online
(status === 'away' && now - awaySince < 180000)
) {
return true
}
return false
}

View File

@ -1,5 +1,6 @@
/* eslint-disable security/detect-object-injection */
import { applyMiddleware } from 'graphql-middleware'
import CONFIG from './../config'
import CONFIG from '../config'
import softDelete from './softDelete/softDeleteMiddleware'
import sluggify from './sluggifyMiddleware'
import excerpt from './excerptMiddleware'
@ -8,6 +9,7 @@ import permissions from './permissionsMiddleware'
import includedFields from './includedFieldsMiddleware'
import orderBy from './orderByMiddleware'
import validation from './validation/validationMiddleware'
// eslint-disable-next-line import/no-cycle
import notifications from './notifications/notificationsMiddleware'
import hashtags from './hashtags/hashtagsMiddleware'
import login from './login/loginMiddleware'

View File

@ -1,8 +1,8 @@
import * as cheerio from 'cheerio'
import { load } from 'cheerio'
export default (content?) => {
if (!content) return []
const $ = cheerio.load(content)
const $ = load(content)
const userIds = $('a.mention[data-mention-id]')
.map((_, el) => {
return $(el).attr('data-mention-id')

View File

@ -1,5 +1,5 @@
import gql from 'graphql-tag'
import { cleanDatabase } from '../../db/factories'
import Factory, { cleanDatabase } from '../../db/factories'
import { createTestClient } from 'apollo-server-testing'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer, { pubsub } from '../../server'
@ -10,6 +10,25 @@ import {
changeGroupMemberRoleMutation,
removeUserFromGroupMutation,
} from '../../graphql/groups'
import { createMessageMutation } from '../../graphql/messages'
import { createRoomMutation } from '../../graphql/rooms'
const sendMailMock = jest.fn()
jest.mock('../helpers/email/sendMail', () => ({
sendMail: () => sendMailMock(),
}))
const chatMessageTemplateMock = jest.fn()
const notificationTemplateMock = jest.fn()
jest.mock('../helpers/email/templateBuilder', () => ({
chatMessageTemplate: () => chatMessageTemplateMock(),
notificationTemplate: () => notificationTemplateMock(),
}))
let isUserOnlineMock = jest.fn()
jest.mock('../helpers/isUserOnline', () => ({
isUserOnline: () => isUserOnlineMock(),
}))
let server, query, mutate, notifiedUser, authenticatedUser
let publishSpy
@ -68,8 +87,8 @@ afterAll(async () => {
beforeEach(async () => {
publishSpy.mockClear()
notifiedUser = await neode.create(
'User',
notifiedUser = await Factory.build(
'user',
{
id: 'you',
name: 'Al Capone',
@ -169,6 +188,7 @@ describe('notifications', () => {
describe('commenter is not me', () => {
beforeEach(async () => {
jest.clearAllMocks()
commentContent = 'Commenters comment.'
commentAuthor = await neode.create(
'User',
@ -184,25 +204,8 @@ describe('notifications', () => {
)
})
it('sends me a notification', async () => {
it('sends me a notification and email', async () => {
await createCommentOnPostAction()
const expected = expect.objectContaining({
data: {
notifications: [
{
read: false,
createdAt: expect.any(String),
reason: 'commented_on_post',
from: {
__typename: 'Comment',
id: 'c47',
content: commentContent,
},
relatedUser: null,
},
],
},
})
await expect(
query({
query: notificationQuery,
@ -210,24 +213,85 @@ describe('notifications', () => {
read: false,
},
}),
).resolves.toEqual(expected)
).resolves.toMatchObject(
expect.objectContaining({
data: {
notifications: [
{
read: false,
createdAt: expect.any(String),
reason: 'commented_on_post',
from: {
__typename: 'Comment',
id: 'c47',
content: commentContent,
},
relatedUser: null,
},
],
},
}),
)
// Mail
expect(sendMailMock).toHaveBeenCalledTimes(1)
expect(notificationTemplateMock).toHaveBeenCalledTimes(1)
})
it('sends me no notification if I have blocked the comment author', async () => {
await notifiedUser.relateTo(commentAuthor, 'blocked')
await createCommentOnPostAction()
const expected = expect.objectContaining({
data: { notifications: [] },
})
describe('if I have disabled `emailNotificationsCommentOnObservedPost`', () => {
it('sends me a notification but no email', async () => {
await notifiedUser.update({ emailNotificationsCommentOnObservedPost: false })
await createCommentOnPostAction()
await expect(
query({
query: notificationQuery,
variables: {
read: false,
},
}),
).resolves.toMatchObject(
expect.objectContaining({
data: {
notifications: [
{
read: false,
createdAt: expect.any(String),
reason: 'commented_on_post',
from: {
__typename: 'Comment',
id: 'c47',
content: commentContent,
},
relatedUser: null,
},
],
},
}),
)
await expect(
query({
query: notificationQuery,
variables: {
read: false,
},
}),
).resolves.toEqual(expected)
// No Mail
expect(sendMailMock).not.toHaveBeenCalled()
expect(notificationTemplateMock).not.toHaveBeenCalled()
})
})
describe('if I have blocked the comment author', () => {
it('sends me no notification', async () => {
await notifiedUser.relateTo(commentAuthor, 'blocked')
await createCommentOnPostAction()
const expected = expect.objectContaining({
data: { notifications: [] },
})
await expect(
query({
query: notificationQuery,
variables: {
read: false,
},
}),
).resolves.toEqual(expected)
})
})
})
@ -256,6 +320,7 @@ describe('notifications', () => {
})
beforeEach(async () => {
jest.clearAllMocks()
postAuthor = await neode.create(
'User',
{
@ -278,7 +343,7 @@ describe('notifications', () => {
'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone">@al-capone</a> how do you do?'
})
it('sends me a notification', async () => {
it('sends me a notification and email', async () => {
await createPostAction()
const expectedContent =
'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do?'
@ -306,6 +371,47 @@ describe('notifications', () => {
],
},
})
// Mail
expect(sendMailMock).toHaveBeenCalledTimes(1)
expect(notificationTemplateMock).toHaveBeenCalledTimes(1)
})
describe('if I have disabled `emailNotificationsMention`', () => {
it('sends me a notification but no email', async () => {
await notifiedUser.update({ emailNotificationsMention: false })
await createPostAction()
const expectedContent =
'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do?'
await expect(
query({
query: notificationQuery,
variables: {
read: false,
},
}),
).resolves.toMatchObject({
errors: undefined,
data: {
notifications: [
{
read: false,
createdAt: expect.any(String),
reason: 'mentioned_in_post',
from: {
__typename: 'Post',
id: 'p47',
content: expectedContent,
},
},
],
},
})
// Mail
expect(sendMailMock).not.toHaveBeenCalled()
expect(notificationTemplateMock).not.toHaveBeenCalled()
})
})
it('publishes `NOTIFICATION_ADDED` to me', async () => {
@ -633,12 +739,121 @@ describe('notifications', () => {
})
})
describe('chat email notifications', () => {
let chatSender
let chatReceiver
let roomId
beforeEach(async () => {
jest.clearAllMocks()
chatSender = await neode.create(
'User',
{
id: 'chatSender',
name: 'chatSender',
slug: 'chatSender',
},
{
email: 'chatSender@example.org',
password: '1234',
},
)
chatReceiver = await Factory.build(
'user',
{ id: 'chatReceiver', name: 'chatReceiver', slug: 'chatReceiver' },
{ email: 'user@example.org' },
)
authenticatedUser = await chatSender.toJson()
const room = await mutate({
mutation: createRoomMutation(),
variables: {
userId: 'chatReceiver',
},
})
roomId = room.data.CreateRoom.id
})
describe('if the chatReceiver is online', () => {
it('sends no email', async () => {
isUserOnlineMock = jest.fn().mockReturnValue(true)
await mutate({
mutation: createMessageMutation(),
variables: {
roomId,
content: 'Some nice message to chatReceiver',
},
})
expect(sendMailMock).not.toHaveBeenCalled()
expect(chatMessageTemplateMock).not.toHaveBeenCalled()
})
})
describe('if the chatReceiver is offline', () => {
it('sends an email', async () => {
isUserOnlineMock = jest.fn().mockReturnValue(false)
await mutate({
mutation: createMessageMutation(),
variables: {
roomId,
content: 'Some nice message to chatReceiver',
},
})
expect(sendMailMock).toHaveBeenCalledTimes(1)
expect(chatMessageTemplateMock).toHaveBeenCalledTimes(1)
})
})
describe('if the chatReceiver has blocked chatSender', () => {
it('sends no email', async () => {
isUserOnlineMock = jest.fn().mockReturnValue(false)
await chatReceiver.relateTo(chatSender, 'blocked')
await mutate({
mutation: createMessageMutation(),
variables: {
roomId,
content: 'Some nice message to chatReceiver',
},
})
expect(sendMailMock).not.toHaveBeenCalled()
expect(chatMessageTemplateMock).not.toHaveBeenCalled()
})
})
describe('if the chatReceiver has disabled `emailNotificationsChatMessage`', () => {
it('sends no email', async () => {
isUserOnlineMock = jest.fn().mockReturnValue(false)
await chatReceiver.update({ emailNotificationsChatMessage: false })
await mutate({
mutation: createMessageMutation(),
variables: {
roomId,
content: 'Some nice message to chatReceiver',
},
})
expect(sendMailMock).not.toHaveBeenCalled()
expect(chatMessageTemplateMock).not.toHaveBeenCalled()
})
})
})
describe('group notifications', () => {
let groupOwner
beforeEach(async () => {
groupOwner = await neode.create(
'User',
groupOwner = await Factory.build(
'user',
{
id: 'group-owner',
name: 'Group Owner',
@ -665,7 +880,7 @@ describe('notifications', () => {
})
describe('user joins group', () => {
beforeEach(async () => {
const joinGroupAction = async () => {
authenticatedUser = await notifiedUser.toJson()
await mutate({
mutation: joinGroupMutation(),
@ -675,9 +890,14 @@ describe('notifications', () => {
},
})
authenticatedUser = await groupOwner.toJson()
}
beforeEach(async () => {
jest.clearAllMocks()
})
it('has the notification in database', async () => {
it('sends the group owner a notification and email', async () => {
await joinGroupAction()
await expect(
query({
query: notificationQuery,
@ -701,19 +921,50 @@ describe('notifications', () => {
},
errors: undefined,
})
// Mail
expect(sendMailMock).toHaveBeenCalledTimes(1)
expect(notificationTemplateMock).toHaveBeenCalledTimes(1)
})
describe('if the group owner has disabled `emailNotificationsGroupMemberJoined`', () => {
it('sends the group owner a notification but no email', async () => {
await groupOwner.update({ emailNotificationsGroupMemberJoined: false })
await joinGroupAction()
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: [
{
read: false,
reason: 'user_joined_group',
createdAt: expect.any(String),
from: {
__typename: 'Group',
id: 'closed-group',
},
relatedUser: {
id: 'you',
},
},
],
},
errors: undefined,
})
// Mail
expect(sendMailMock).not.toHaveBeenCalled()
expect(notificationTemplateMock).not.toHaveBeenCalled()
})
})
})
describe('user leaves group', () => {
beforeEach(async () => {
describe('user joins and leaves group', () => {
const leaveGroupAction = async () => {
authenticatedUser = await notifiedUser.toJson()
await mutate({
mutation: joinGroupMutation(),
variables: {
groupId: 'closed-group',
userId: authenticatedUser.id,
},
})
await mutate({
mutation: leaveGroupMutation(),
variables: {
@ -722,9 +973,22 @@ describe('notifications', () => {
},
})
authenticatedUser = await groupOwner.toJson()
}
beforeEach(async () => {
jest.clearAllMocks()
authenticatedUser = await notifiedUser.toJson()
await mutate({
mutation: joinGroupMutation(),
variables: {
groupId: 'closed-group',
userId: authenticatedUser.id,
},
})
})
it('has two the notification in database', async () => {
it('sends the group owner two notifications and emails', async () => {
await leaveGroupAction()
await expect(
query({
query: notificationQuery,
@ -760,19 +1024,61 @@ describe('notifications', () => {
},
errors: undefined,
})
// Mail
expect(sendMailMock).toHaveBeenCalledTimes(2)
expect(notificationTemplateMock).toHaveBeenCalledTimes(2)
})
describe('if the group owner has disabled `emailNotificationsGroupMemberLeft`', () => {
it('sends the group owner two notification but only only one email', async () => {
await groupOwner.update({ emailNotificationsGroupMemberLeft: false })
await leaveGroupAction()
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: [
{
read: false,
reason: 'user_left_group',
createdAt: expect.any(String),
from: {
__typename: 'Group',
id: 'closed-group',
},
relatedUser: {
id: 'you',
},
},
{
read: false,
reason: 'user_joined_group',
createdAt: expect.any(String),
from: {
__typename: 'Group',
id: 'closed-group',
},
relatedUser: {
id: 'you',
},
},
],
},
errors: undefined,
})
// Mail
expect(sendMailMock).toHaveBeenCalledTimes(1)
expect(notificationTemplateMock).toHaveBeenCalledTimes(1)
})
})
})
describe('user role in group changes', () => {
beforeEach(async () => {
authenticatedUser = await notifiedUser.toJson()
await mutate({
mutation: joinGroupMutation(),
variables: {
groupId: 'closed-group',
userId: authenticatedUser.id,
},
})
const changeGroupMemberRoleAction = async () => {
authenticatedUser = await groupOwner.toJson()
await mutate({
mutation: changeGroupMemberRoleMutation(),
@ -783,9 +1089,23 @@ describe('notifications', () => {
},
})
authenticatedUser = await notifiedUser.toJson()
}
beforeEach(async () => {
authenticatedUser = await notifiedUser.toJson()
await mutate({
mutation: joinGroupMutation(),
variables: {
groupId: 'closed-group',
userId: authenticatedUser.id,
},
})
// Clear after because the above generates a notification not related
jest.clearAllMocks()
})
it('has notification in database', async () => {
it('sends the group member a notification and email', async () => {
await changeGroupMemberRoleAction()
await expect(
query({
query: notificationQuery,
@ -809,19 +1129,49 @@ describe('notifications', () => {
},
errors: undefined,
})
// Mail
expect(sendMailMock).toHaveBeenCalledTimes(1)
expect(notificationTemplateMock).toHaveBeenCalledTimes(1)
})
describe('if the group member has disabled `emailNotificationsGroupMemberRoleChanged`', () => {
it('sends the group member a notification but no email', async () => {
notifiedUser.update({ emailNotificationsGroupMemberRoleChanged: false })
await changeGroupMemberRoleAction()
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: [
{
read: false,
reason: 'changed_group_member_role',
createdAt: expect.any(String),
from: {
__typename: 'Group',
id: 'closed-group',
},
relatedUser: {
id: 'group-owner',
},
},
],
},
errors: undefined,
})
// Mail
expect(sendMailMock).not.toHaveBeenCalled()
expect(notificationTemplateMock).not.toHaveBeenCalled()
})
})
})
describe('user is removed from group', () => {
beforeEach(async () => {
authenticatedUser = await notifiedUser.toJson()
await mutate({
mutation: joinGroupMutation(),
variables: {
groupId: 'closed-group',
userId: authenticatedUser.id,
},
})
const removeUserFromGroupAction = async () => {
authenticatedUser = await groupOwner.toJson()
await mutate({
mutation: removeUserFromGroupMutation(),
@ -831,9 +1181,23 @@ describe('notifications', () => {
},
})
authenticatedUser = await notifiedUser.toJson()
}
beforeEach(async () => {
authenticatedUser = await notifiedUser.toJson()
await mutate({
mutation: joinGroupMutation(),
variables: {
groupId: 'closed-group',
userId: authenticatedUser.id,
},
})
// Clear after because the above generates a notification not related
jest.clearAllMocks()
})
it('has notification in database', async () => {
it('sends the previous group member a notification and email', async () => {
await removeUserFromGroupAction()
await expect(
query({
query: notificationQuery,
@ -857,6 +1221,44 @@ describe('notifications', () => {
},
errors: undefined,
})
// Mail
expect(sendMailMock).toHaveBeenCalledTimes(1)
expect(notificationTemplateMock).toHaveBeenCalledTimes(1)
})
describe('if the previous group member has disabled `emailNotificationsGroupMemberRemoved`', () => {
it('sends the previous group member a notification but no email', async () => {
notifiedUser.update({ emailNotificationsGroupMemberRemoved: false })
await removeUserFromGroupAction()
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: [
{
read: false,
reason: 'removed_user_from_group',
createdAt: expect.any(String),
from: {
__typename: 'Group',
id: 'closed-group',
},
relatedUser: {
id: 'group-owner',
},
},
],
},
errors: undefined,
})
// Mail
expect(sendMailMock).not.toHaveBeenCalled()
expect(notificationTemplateMock).not.toHaveBeenCalled()
})
})
})
})

View File

@ -1,8 +1,11 @@
/* eslint-disable security/detect-object-injection */
// eslint-disable-next-line import/no-cycle
import { pubsub, NOTIFICATION_ADDED } from '../../server'
import extractMentionedUsers from './mentions/extractMentionedUsers'
import { validateNotifyUsers } from '../validation/validationMiddleware'
import { sendMail } from '../helpers/email/sendMail'
import { notificationTemplate } from '../helpers/email/templateBuilder'
import { chatMessageTemplate, notificationTemplate } from '../helpers/email/templateBuilder'
import { isUserOnline } from '../helpers/isUserOnline'
const queryNotificationEmails = async (context, notificationUserIds) => {
if (!(notificationUserIds && notificationUserIds.length)) return []
@ -41,7 +44,6 @@ const publishNotifications = async (context, promises, emailNotificationSetting:
notifications.forEach((notificationAdded, index) => {
pubsub.publish(NOTIFICATION_ADDED, { notificationAdded })
if (notificationAdded.to[emailNotificationSetting] ?? true) {
// Default to true
sendMail(
notificationTemplate({
email: notificationsEmailAddresses[index].email,
@ -335,6 +337,56 @@ const notifyUsersOfComment = async (label, commentId, reason, context) => {
}
}
const handleCreateMessage = async (resolve, root, args, context, resolveInfo) => {
// Execute resolver
const result = await resolve(root, args, context, resolveInfo)
// Query Parameters
const { roomId } = args
const {
user: { id: currentUserId },
} = context
// Find Recipient
const session = context.driver.session()
const messageRecipient = session.readTransaction(async (transaction) => {
const messageRecipientCypher = `
MATCH (currentUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room { id: $roomId })
MATCH (room)<-[:CHATS_IN]-(recipientUser:User)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress)
WHERE NOT recipientUser.id = $currentUserId
AND NOT (recipientUser)-[:BLOCKED]-(currentUser)
AND NOT recipientUser.emailNotificationsChatMessage = false
RETURN recipientUser, emailAddress {.email}
`
const txResponse = await transaction.run(messageRecipientCypher, {
currentUserId,
roomId,
})
return {
user: await txResponse.records.map((record) => record.get('recipientUser'))[0],
email: await txResponse.records.map((record) => record.get('emailAddress'))[0]?.email,
}
})
try {
// Execute Query
const { user, email } = await messageRecipient
// Send EMail if we found a user(not blocked) and he is not considered online
if (user && !isUserOnline(user)) {
void sendMail(chatMessageTemplate({ email, variables: { name: user.properties.name } }))
}
// Return resolver result to client
return result
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
}
export default {
Mutation: {
CreatePost: handleContentDataOfPost,
@ -345,5 +397,6 @@ export default {
LeaveGroup: handleLeaveGroup,
ChangeGroupMemberRole: handleChangeGroupMemberRole,
RemoveUserFromGroup: handleRemoveUserFromGroup,
CreateMessage: handleCreateMessage,
},
}

View File

@ -1,6 +1,7 @@
import { sentry } from 'graphql-middleware-sentry'
import CONFIG from '../config'
// eslint-disable-next-line import/no-mutable-exports
let sentryMiddleware: any = (resolve, root, args, context, resolveInfo) =>
resolve(root, args, context, resolveInfo)

View File

@ -1,5 +1,5 @@
import walkRecursive from '../helpers/walkRecursive'
import { cleanHtml } from '../middleware/helpers/cleanHtml'
import { cleanHtml } from './helpers/cleanHtml'
// exclamation mark separetes field names, that should not be sanitized
const fields = [

View File

@ -165,6 +165,10 @@ export default {
type: 'boolean',
default: true,
},
emailNotificationsChatMessage: {
type: 'boolean',
default: true,
},
emailNotificationsGroupMemberJoined: {
type: 'boolean',
default: true,

View File

@ -1,3 +1,5 @@
/* eslint-disable n/no-missing-require */
/* eslint-disable n/global-require */
// NOTE: We cannot use `fs` here to clean up the code. Cypress breaks on any npm
// module that is not browser-compatible. Node's `fs` module is server-side only
declare let Cypress: any | undefined

View File

@ -2,6 +2,7 @@ import generateNonce from './helpers/generateNonce'
import Resolver from './helpers/Resolver'
import existingEmailAddress from './helpers/existingEmailAddress'
import { UserInputError } from 'apollo-server'
// eslint-disable-next-line import/extensions
import Validator from 'neode/build/Services/Validator.js'
import normalizeEmail from './helpers/normalizeEmail'

View File

@ -1,3 +1,7 @@
/* eslint-disable n/no-extraneous-require */
/* eslint-disable n/global-require */
/* eslint-disable import/no-commonjs */
/* eslint-disable import/no-named-as-default */
import Metascraper from 'metascraper'
import fetch from 'node-fetch'
@ -37,6 +41,7 @@ const fetchEmbed = async (url) => {
try {
const response = await fetch(endpointUrl)
json = await response.json()
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (err) {
error(`Error fetching embed data: ${err.message}`)
return {}

View File

@ -1,3 +1,4 @@
/* eslint-disable security/detect-object-injection */
import log from './databaseLogger'
export const undefinedToNullResolver = (list) => {

View File

@ -1,4 +1,6 @@
/* eslint-disable import/no-named-as-default */
import Debug from 'debug'
const debugCypher = Debug('human-connection:neo4j:cypher')
const debugStats = Debug('human-connection:neo4j:stats')

View File

@ -1,4 +1,4 @@
import CONSTANTS_REGISTRATION from './../../../constants/registration'
import CONSTANTS_REGISTRATION from '../../../constants/registration'
export default function generateInviteCode() {
// 6 random numbers in [ 0, 35 ] are 36 possible numbers (10 [0-9] + 26 [A-Z])

View File

@ -1,4 +1,4 @@
import CONSTANTS_REGISTRATION from './../../../constants/registration'
import CONSTANTS_REGISTRATION from '../../../constants/registration'
// TODO: why this is not used in resolver 'requestPasswordReset'?
export default function generateNonce() {

View File

@ -1,4 +1,5 @@
import Resolver from './helpers/Resolver'
export default {
Image: {
...Resolver('Image', {

View File

@ -1,3 +1,4 @@
/* eslint-disable promise/prefer-await-to-callbacks */
import { deleteImage, mergeImage } from './images'
import { getNeode, getDriver } from '../../../db/neo4j'
import Factory, { cleanDatabase } from '../../../db/factories'
@ -90,6 +91,7 @@ describe('deleteImage', () => {
})
throw new Error('Ouch!')
})
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (err) {
// nothing has been deleted
await expect(neode.all('Image')).resolves.toHaveLength(1)
@ -251,6 +253,7 @@ describe('mergeImage', () => {
})
return transaction.run('Ooops invalid cypher!', { image })
})
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (err) {
// nothing has been created
await expect(neode.all('Image')).resolves.toHaveLength(0)

View File

@ -1,3 +1,5 @@
/* eslint-disable promise/avoid-new */
/* eslint-disable security/detect-non-literal-fs-filename */
import path from 'path'
import { v4 as uuid } from 'uuid'
import { S3 } from 'aws-sdk'

View File

@ -1,9 +1,10 @@
/* eslint-disable security/detect-non-literal-regexp */
import Factory, { cleanDatabase } from '../../db/factories'
import { getDriver } from '../../db/neo4j'
import gql from 'graphql-tag'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'
import CONSTANTS_REGISTRATION from './../../constants/registration'
import CONSTANTS_REGISTRATION from '../../constants/registration'
let user
let query

View File

@ -2,7 +2,7 @@ import Factory, { cleanDatabase } from '../../db/factories'
import gql from 'graphql-tag'
import { getDriver } from '../../db/neo4j'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../.././server'
import createServer from '../../server'
import {
markAsReadMutation,
markAllAsReadMutation,

View File

@ -1,7 +1,7 @@
import Factory, { cleanDatabase } from '../../db/factories'
import gql from 'graphql-tag'
import { getNeode, getDriver } from '../../db/neo4j'
import CONSTANTS_REGISTRATION from './../../constants/registration'
import CONSTANTS_REGISTRATION from '../../constants/registration'
import createPasswordReset from './helpers/createPasswordReset'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'

View File

@ -1,6 +1,6 @@
import { v4 as uuid } from 'uuid'
import bcrypt from 'bcryptjs'
import CONSTANTS_REGISTRATION from './../../constants/registration'
import CONSTANTS_REGISTRATION from '../../constants/registration'
import createPasswordReset from './helpers/createPasswordReset'
export default {

View File

@ -1,5 +1,5 @@
import { createTestClient } from 'apollo-server-testing'
import createServer from '../.././server'
import createServer from '../../server'
import Factory, { cleanDatabase } from '../../db/factories'
import gql from 'graphql-tag'
import { getDriver, getNeode } from '../../db/neo4j'

View File

@ -1,3 +1,4 @@
/* eslint-disable security/detect-object-injection */
import log from './helpers/databaseLogger'
export default {

View File

@ -1,5 +1,6 @@
/* eslint-disable promise/prefer-await-to-callbacks */
import jwt from 'jsonwebtoken'
import CONFIG from './../../config'
import CONFIG from '../../config'
import Factory, { cleanDatabase } from '../../db/factories'
import gql from 'graphql-tag'
import { loginMutation } from '../../graphql/userManagement'

View File

@ -676,6 +676,15 @@ describe('emailNotificationSettings', () => {
},
],
},
{
type: 'chat',
settings: [
{
name: 'chatMessage',
value: true,
},
],
},
{
type: 'group',
settings: [
@ -760,6 +769,15 @@ describe('emailNotificationSettings', () => {
},
],
},
{
type: 'chat',
settings: [
{
name: 'chatMessage',
value: true,
},
],
},
{
type: 'group',
settings: [

View File

@ -160,8 +160,7 @@ export default {
const allowedSettingNames = [
'commentOnObservedPost',
'mention',
'postByFollowedUser',
'postInGroup',
'chatMessage',
'groupMemberJoined',
'groupMemberLeft',
'groupMemberRemoved',
@ -396,6 +395,15 @@ export default {
},
],
},
{
type: 'chat',
settings: [
{
name: 'chatMessage',
value: parent.emailNotificationsChatMessage ?? true,
},
],
},
{
type: 'group',
settings: [

View File

@ -1,3 +1,6 @@
/* eslint-disable promise/avoid-new */
/* eslint-disable promise/prefer-await-to-callbacks */
/* eslint-disable import/no-named-as-default */
import request from 'request'
import { UserInputError } from 'apollo-server'
import Debug from 'debug'

View File

@ -1,8 +1,10 @@
/* eslint-disable import/no-named-as-default-member */
import express from 'express'
import http from 'http'
import helmet from 'helmet'
import { ApolloServer } from 'apollo-server-express'
import CONFIG from './config'
// eslint-disable-next-line import/no-cycle
import middleware from './middleware'
import { getNeode, getDriver } from './db/neo4j'
import decode from './jwt/decode'

View File

@ -1091,6 +1091,14 @@
dependencies:
tslib "^2.4.0"
"@eslint-community/eslint-plugin-eslint-comments@^4.4.1":
version "4.4.1"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-4.4.1.tgz#dbfab6f2447c22be8758a0a9a9c80e56d2e2b93f"
integrity sha512-lb/Z/MzbTf7CaVYM9WCFNQZ4L1yi3ev2fsFPF99h31ljhSEyUoyEsKsNWiU+qD1glbYTDJdqgyaLKtyTkkqtuQ==
dependencies:
escape-string-regexp "^4.0.0"
ignore "^5.2.4"
"@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
version "4.4.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
@ -4606,6 +4614,11 @@ eslint-plugin-n@^16.6.2:
resolve "^1.22.2"
semver "^7.5.3"
eslint-plugin-no-catch-all@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-no-catch-all/-/eslint-plugin-no-catch-all-1.1.0.tgz#f2e8950cc2b0bdde5faa4ab339d0986c6ae32fb0"
integrity sha512-VkP62jLTmccPrFGN/W6V7a3SEwdtTZm+Su2k4T3uyJirtkm0OMMm97h7qd8pRFAHus/jQg9FpUpLRc7sAylBEQ==
eslint-plugin-prettier@^5.2.6:
version "5.2.6"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz#be39e3bb23bb3eeb7e7df0927cdb46e4d7945096"
@ -7353,29 +7366,29 @@ migrate@^2.1.0:
mkdirp "^3.0.1"
slug "^8.2.2"
mime-db@1.43.0:
version "1.43.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58"
integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12, mime-types@^2.1.35, mime-types@~2.1.19, mime-types@~2.1.34:
mime-db@^1.54.0:
version "1.54.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5"
integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==
mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.22, mime-types@~2.1.24, mime-types@~2.1.34:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
mime-types@~2.1.22, mime-types@~2.1.24:
version "2.1.26"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06"
integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==
mime-types@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.1.tgz#b1d94d6997a9b32fd69ebaed0db73de8acb519ce"
integrity sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==
dependencies:
mime-db "1.43.0"
mime-db "^1.54.0"
mime@1.6.0:
version "1.6.0"
@ -7534,10 +7547,10 @@ n-gram@^1.0.0:
resolved "https://registry.yarnpkg.com/n-gram/-/n-gram-1.1.1.tgz#a374dc176a9063a2388d1be18ed7c35828be2a97"
integrity sha512-qibRqvUghLIVsq+RTwVuwOzgOxf0l4DDZKVYAK0bMam5sG9ZzaJ6BUSJyG2Td8kTc7c/HcMUtjiN5ShobZA2bA==
nan@2.17.0, nan@^2.20.0:
version "2.17.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
nan@^2.20.0:
version "2.22.2"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.2.tgz#6b504fd029fb8f38c0990e52ad5c26772fdacfbb"
integrity sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==
nanoid@^3.3.6:
version "3.3.7"