diff --git a/.eslintignore b/.eslintignore index b98800ed6f..f06235c460 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,2 @@ node_modules dist -app/vite.config.js diff --git a/.eslintrc.js b/.eslintrc.js index f0428946c8..d641f4a30e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,9 +1,9 @@ const defaultRules = { // No console statements in production - 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', + 'no-console': process.env.NODE_ENV !== 'development' ? 'error' : 'off', // No debugger statements in production - 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', - // Enforce prettier formating + 'no-debugger': process.env.NODE_ENV !== 'development' ? 'error' : 'off', + // Enforce prettier formatting 'prettier/prettier': 'error', }; diff --git a/.prettierignore b/.prettierignore index de4d1f007d..5b1177ee53 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,3 @@ dist node_modules +app/src/lang/translations/*.yaml diff --git a/api/package.json b/api/package.json index 830098f2e7..bc1adad6e6 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "directus", - "version": "9.0.0-rc.75", + "version": "9.0.0-rc.76", "license": "GPL-3.0-only", "homepage": "https://github.com/directus/directus#readme", "description": "Directus is a real-time API and App dashboard for managing SQL database content.", @@ -66,14 +66,14 @@ "example.env" ], "dependencies": { - "@directus/app": "9.0.0-rc.75", - "@directus/drive": "9.0.0-rc.75", - "@directus/drive-azure": "9.0.0-rc.75", - "@directus/drive-gcs": "9.0.0-rc.75", - "@directus/drive-s3": "9.0.0-rc.75", - "@directus/format-title": "9.0.0-rc.75", - "@directus/schema": "9.0.0-rc.75", - "@directus/specs": "9.0.0-rc.75", + "@directus/app": "9.0.0-rc.76", + "@directus/drive": "9.0.0-rc.76", + "@directus/drive-azure": "9.0.0-rc.76", + "@directus/drive-gcs": "9.0.0-rc.76", + "@directus/drive-s3": "9.0.0-rc.76", + "@directus/format-title": "9.0.0-rc.76", + "@directus/schema": "9.0.0-rc.76", + "@directus/specs": "9.0.0-rc.76", "@godaddy/terminus": "^4.9.0", "argon2": "^0.28.1", "async": "^3.2.0", @@ -102,7 +102,7 @@ "graphql": "^15.5.0", "graphql-compose": "^9.0.1", "icc": "^2.0.0", - "inquirer": "^8.1.0", + "inquirer": "^8.1.1", "joi": "^17.3.0", "js-yaml": "^4.1.0", "js2xmlparser": "^4.0.1", @@ -125,6 +125,7 @@ "otplib": "^12.0.1", "pino": "^6.11.3", "pino-colada": "^2.1.0", + "prettier": "^2.3.1", "qs": "^6.9.4", "rate-limiter-flexible": "^2.2.2", "resolve-cwd": "^3.0.0", @@ -138,7 +139,7 @@ "connect-memcached": "^1.0.0", "connect-redis": "^6.0.0", "connect-session-knex": "^2.1.0", - "ioredis": "^4.27.2", + "ioredis": "^4.27.6", "keyv-memcache": "^1.2.5", "memcached": "^2.2.2", "mysql": "^2.18.1", diff --git a/api/src/cli/commands/count/index.ts b/api/src/cli/commands/count/index.ts index 3a90a1763a..62a7021eef 100644 --- a/api/src/cli/commands/count/index.ts +++ b/api/src/cli/commands/count/index.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-console */ + import getDatabase from '../../../database'; export default async function count(collection: string): Promise { diff --git a/api/src/cli/commands/database/install.ts b/api/src/cli/commands/database/install.ts index e71b75f2b9..48fbc1c105 100644 --- a/api/src/cli/commands/database/install.ts +++ b/api/src/cli/commands/database/install.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-console */ + import runMigrations from '../../../database/migrations/run'; import installSeeds from '../../../database/seeds/run'; import getDatabase from '../../../database'; diff --git a/api/src/cli/commands/database/migrate.ts b/api/src/cli/commands/database/migrate.ts index 36784f47c8..8c5b32a17f 100644 --- a/api/src/cli/commands/database/migrate.ts +++ b/api/src/cli/commands/database/migrate.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-console */ + import run from '../../../database/migrations/run'; import getDatabase from '../../../database'; diff --git a/api/src/cli/commands/init/index.ts b/api/src/cli/commands/init/index.ts index 0388c3aae8..ed5aca4dad 100644 --- a/api/src/cli/commands/init/index.ts +++ b/api/src/cli/commands/init/index.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-console */ + import argon2 from 'argon2'; import chalk from 'chalk'; import execa from 'execa'; diff --git a/api/src/cli/commands/roles/create.ts b/api/src/cli/commands/roles/create.ts index c90c6db557..ab60884feb 100644 --- a/api/src/cli/commands/roles/create.ts +++ b/api/src/cli/commands/roles/create.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-console */ + import { getSchema } from '../../../utils/get-schema'; import { RolesService } from '../../../services'; import getDatabase from '../../../database'; diff --git a/api/src/cli/commands/users/create.ts b/api/src/cli/commands/users/create.ts index 2de4bf9248..348eca565e 100644 --- a/api/src/cli/commands/users/create.ts +++ b/api/src/cli/commands/users/create.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-console */ + import { getSchema } from '../../../utils/get-schema'; import { UsersService } from '../../../services'; import getDatabase from '../../../database'; diff --git a/api/src/cli/commands/users/passwd.ts b/api/src/cli/commands/users/passwd.ts index 5042b28c6c..ca40cbfba5 100644 --- a/api/src/cli/commands/users/passwd.ts +++ b/api/src/cli/commands/users/passwd.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-console */ + import argon2 from 'argon2'; import { getSchema } from '../../../utils/get-schema'; import { UsersService } from '../../../services'; diff --git a/api/src/cli/index.ts b/api/src/cli/index.ts index 462f611376..f68bbe62c5 100644 --- a/api/src/cli/index.ts +++ b/api/src/cli/index.ts @@ -1,5 +1,7 @@ #!/usr/bin/env node +/* eslint-disable no-console */ + import program from 'commander'; import start from '../start'; import bootstrap from './commands/bootstrap'; diff --git a/api/src/database/index.ts b/api/src/database/index.ts index af982c32bf..61824a64bd 100644 --- a/api/src/database/index.ts +++ b/api/src/database/index.ts @@ -28,7 +28,11 @@ export default function getDatabase(): Knex { if (env.DB_CLIENT && env.DB_CLIENT === 'sqlite3') { requiredEnvVars.push('DB_FILENAME'); } else if (env.DB_CLIENT && env.DB_CLIENT === 'oracledb') { - requiredEnvVars.push('DB_USER', 'DB_PASSWORD', 'DB_CONNECT_STRING'); + if (!env.DB_CONNECT_STRING) { + requiredEnvVars.push('DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USER', 'DB_PASSWORD'); + } else { + requiredEnvVars.push('DB_USER', 'DB_PASSWORD', 'DB_CONNECT_STRING'); + } } else { if (env.DB_CLIENT === 'pg') { if (!env.DB_CONNECTION_STRING) { diff --git a/api/src/database/migrations/20201105B-change-webhook-url-type.ts b/api/src/database/migrations/20201105B-change-webhook-url-type.ts index d1ad118cc5..3107d4d9dd 100644 --- a/api/src/database/migrations/20201105B-change-webhook-url-type.ts +++ b/api/src/database/migrations/20201105B-change-webhook-url-type.ts @@ -1,4 +1,6 @@ import { Knex } from 'knex'; +// @ts-ignore +import Client_Oracledb from 'knex/lib/dialects/oracledb'; import env from '../../env'; async function oracleAlterUrl(knex: Knex, type: string): Promise { @@ -10,7 +12,7 @@ async function oracleAlterUrl(knex: Knex, type: string): Promise { } export async function up(knex: Knex): Promise { - if (env.DB_CLIENT === 'oracledb') { + if (knex.client instanceof Client_Oracledb) { await oracleAlterUrl(knex, 'CLOB'); return; } diff --git a/api/src/database/migrations/20210312A-webhooks-collections-text.ts b/api/src/database/migrations/20210312A-webhooks-collections-text.ts index 09cd50b5c1..489e0253f5 100644 --- a/api/src/database/migrations/20210312A-webhooks-collections-text.ts +++ b/api/src/database/migrations/20210312A-webhooks-collections-text.ts @@ -1,4 +1,6 @@ import { Knex } from 'knex'; +// @ts-ignore +import Client_Oracledb from 'knex/lib/dialects/oracledb'; import env from '../../env'; async function oracleAlterCollections(knex: Knex, type: string): Promise { @@ -10,7 +12,7 @@ async function oracleAlterCollections(knex: Knex, type: string): Promise { } export async function up(knex: Knex): Promise { - if (env.DB_CLIENT === 'oracledb') { + if (knex.client instanceof Client_Oracledb) { await oracleAlterCollections(knex, 'CLOB'); return; } diff --git a/api/src/database/migrations/run.ts b/api/src/database/migrations/run.ts index 95c4f2c316..5b3566fd1d 100644 --- a/api/src/database/migrations/run.ts +++ b/api/src/database/migrations/run.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-console */ + import formatTitle from '@directus/format-title'; import fse from 'fs-extra'; import { Knex } from 'knex'; diff --git a/api/src/env.ts b/api/src/env.ts index 5873a5e1de..96277e67ee 100644 --- a/api/src/env.ts +++ b/api/src/env.ts @@ -50,6 +50,7 @@ const defaults: Record = { CACHE_TTL: '10m', CACHE_NAMESPACE: 'system-cache', CACHE_AUTO_PURGE: false, + CACHE_CONTROL_S_MAXAGE: '0', OAUTH_PROVIDERS: '', diff --git a/api/src/middleware/cache.ts b/api/src/middleware/cache.ts index 483317226e..0f47869b5c 100644 --- a/api/src/middleware/cache.ts +++ b/api/src/middleware/cache.ts @@ -2,6 +2,7 @@ import { RequestHandler } from 'express'; import cache from '../cache'; import env from '../env'; import asyncHandler from '../utils/async-handler'; +import { getCacheControlHeader } from '../utils/get-cache-headers'; import { getCacheKey } from '../utils/get-cache-key'; const checkCacheMiddleware: RequestHandler = asyncHandler(async (req, res, next) => { @@ -17,18 +18,11 @@ const checkCacheMiddleware: RequestHandler = asyncHandler(async (req, res, next) const cachedData = await cache.get(key); if (cachedData) { - // Set cache-control header, but only for the public role - if (env.CACHE_AUTO_PURGE !== true && !!req.accountability?.role === false) { - const expiresAt = await cache.get(`${key}__expires_at`); - const maxAge = `max-age=${expiresAt - Date.now()}`; - res.setHeader('Cache-Control', `public, ${maxAge}`); - } else { - // This indicates that the browser/proxy is allowed to cache, but has to revalidate with - // the server before use. At this point, we don't include Last-Modified, so it'll always - // recreate the local cache. This does NOT mean that cache is disabled all together, as - // Directus is still pulling the value from it's internal cache. - res.setHeader('Cache-Control', 'no-cache'); - } + const cacheExpiryDate = (await cache.get(`${key}__expires_at`)) as number | null; + const cacheTTL = cacheExpiryDate ? cacheExpiryDate - Date.now() : null; + + res.setHeader('Cache-Control', getCacheControlHeader(req, cacheTTL)); + res.setHeader('Vary', 'Origin, Cache-Control'); return res.json(cachedData); } else { diff --git a/api/src/middleware/respond.ts b/api/src/middleware/respond.ts index 11a81eb215..748d5e48ff 100644 --- a/api/src/middleware/respond.ts +++ b/api/src/middleware/respond.ts @@ -7,6 +7,7 @@ import env from '../env'; import asyncHandler from '../utils/async-handler'; import { getCacheKey } from '../utils/get-cache-key'; import { parse as toXML } from 'js2xmlparser'; +import { getCacheControlHeader } from '../utils/get-cache-headers'; export const respond: RequestHandler = asyncHandler(async (req, res) => { if ( @@ -19,20 +20,12 @@ export const respond: RequestHandler = asyncHandler(async (req, res) => { const key = getCacheKey(req); await cache.set(key, res.locals.payload, ms(env.CACHE_TTL as string)); await cache.set(`${key}__expires_at`, Date.now() + ms(env.CACHE_TTL as string)); - - const noCacheRequested = - req.headers['cache-control']?.includes('no-cache') || req.headers['Cache-Control']?.includes('no-cache'); - - // Set cache-control header - if (env.CACHE_AUTO_PURGE !== true && noCacheRequested === false) { - const maxAge = `max-age=${ms(env.CACHE_TTL as string)}`; - const access = !!req.accountability?.role === false ? 'public' : 'private'; - res.setHeader('Cache-Control', `${access}, ${maxAge}`); - } - - if (noCacheRequested) { - res.setHeader('Cache-Control', 'no-cache'); - } + res.setHeader('Cache-Control', getCacheControlHeader(req, ms(env.CACHE_TTL as string))); + res.setHeader('Vary', 'Origin, Cache-Control'); + } else { + // Don't cache anything by default + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Vary', 'Origin, Cache-Control'); } if (req.sanitizedQuery.export) { diff --git a/api/src/services/mail/index.ts b/api/src/services/mail/index.ts index 4569a5870e..42c3c16f03 100644 --- a/api/src/services/mail/index.ts +++ b/api/src/services/mail/index.ts @@ -9,6 +9,7 @@ import logger from '../../logger'; import { AbstractServiceOptions, Accountability, SchemaOverview } from '../../types'; import getMailer from '../../mailer'; import { Transporter, SendMailOptions } from 'nodemailer'; +import prettier from 'prettier'; const liquidEngine = new Liquid({ root: [path.resolve(env.EXTENSIONS_PATH, 'templates'), path.resolve(__dirname, 'templates')], @@ -61,6 +62,11 @@ export class MailService { html = await this.renderTemplate(template.name, templateData); } + if (typeof html === 'string') { + // Some email clients start acting funky when line length exceeds 75 characters. See #6074 + html = prettier.format(html as string, { parser: 'html', printWidth: 70, tabWidth: 0 }); + } + try { await this.mailer.sendMail({ ...emailOptions, from, html }); } catch (error) { diff --git a/api/src/utils/adjust-date.ts b/api/src/utils/adjust-date.ts index e83301628a..5d631cffdf 100644 --- a/api/src/utils/adjust-date.ts +++ b/api/src/utils/adjust-date.ts @@ -24,7 +24,7 @@ import { clone } from 'lodash'; * * The conversion is lifted straight from `ms`. */ -export function adjustDate(date: Date, adjustment: string) { +export function adjustDate(date: Date, adjustment: string): Date | undefined { date = clone(date); const subtract = adjustment.startsWith('-'); diff --git a/api/src/utils/get-cache-headers.ts b/api/src/utils/get-cache-headers.ts new file mode 100644 index 0000000000..8dad8ab3db --- /dev/null +++ b/api/src/utils/get-cache-headers.ts @@ -0,0 +1,42 @@ +import env from '../env'; +import { Request } from 'express'; + +/** + * Returns the Cache-Control header for the current request + * + * @param req Express request object + * @param ttl TTL of the cache in ms + */ +export function getCacheControlHeader(req: Request, ttl: number | null): string { + // When the resource / current request isn't cached + if (ttl === null) return 'no-cache'; + + // When the API cache can invalidate at any moment + if (env.CACHE_AUTO_PURGE === true) return 'no-cache'; + + const noCacheRequested = + req.headers['cache-control']?.includes('no-cache') || req.headers['Cache-Control']?.includes('no-cache'); + + // When the user explicitly asked to skip the cache + if (noCacheRequested) return 'no-cache'; + + // Cache control header uses seconds for everything + const ttlSeconds = Math.round(ttl / 1000); + + const access = !!req.accountability?.role === false ? 'public' : 'private'; + + let headerValue = `${access}, max-age=${ttlSeconds}`; + + // When the s-maxage flag should be included + if (env.CACHE_CONTROL_S_MAXAGE !== false) { + // Default to regular max-age flag when true + if (env.CACHE_CONTROL_S_MAXAGE === true) { + headerValue += `, s-maxage=${ttlSeconds}`; + } else { + // Set to custom value + headerValue += `, s-maxage=${env.CACHE_CONTROL_S_MAXAGE}`; + } + } + + return headerValue; +} diff --git a/api/src/utils/is-url-allowed.ts b/api/src/utils/is-url-allowed.ts index 52a9018914..3604ebdbb7 100644 --- a/api/src/utils/is-url-allowed.ts +++ b/api/src/utils/is-url-allowed.ts @@ -5,8 +5,6 @@ import logger from '../logger'; * Check if url matches allow list either exactly or by domain+path */ export default function isUrlAllowed(url: string, allowList: string | string[]): boolean { - console.log(url, allowList); - const urlAllowList = toArray(allowList); if (urlAllowList.includes(url)) return true; diff --git a/app/package.json b/app/package.json index 60bc2ee700..dfab22a762 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "@directus/app", - "version": "9.0.0-rc.75", + "version": "9.0.0-rc.76", "private": false, "description": "Directus is an Open-Source Headless CMS & API for Managing Custom Databases", "author": "Rijk van Zanten ", @@ -28,8 +28,8 @@ }, "gitHead": "24621f3934dc77eb23441331040ed13c676ceffd", "devDependencies": { - "@directus/docs": "9.0.0-rc.75", - "@directus/format-title": "9.0.0-rc.75", + "@directus/docs": "9.0.0-rc.76", + "@directus/format-title": "9.0.0-rc.76", "@fullcalendar/core": "^5.7.2", "@fullcalendar/daygrid": "^5.7.2", "@fullcalendar/interaction": "^5.7.2", @@ -38,7 +38,7 @@ "@popperjs/core": "^2.9.2", "@rollup/plugin-yaml": "^3.0.0", "@sindresorhus/slugify": "^2.1.0", - "@tinymce/tinymce-vue": "^4.0.0", + "@tinymce/tinymce-vue": "^4.0.3", "@types/base-64": "^1.0.0", "@types/bytes": "^3.1.0", "@types/codemirror": "^5.60.0", @@ -62,7 +62,7 @@ "base-64": "^1.0.0", "codemirror": "^5.61.1", "copyfiles": "^2.4.1", - "cropperjs": "^1.5.11", + "cropperjs": "^1.5.12", "date-fns": "^2.21.1", "dompurify": "^2.2.9", "escape-string-regexp": "^5.0.0", @@ -79,7 +79,7 @@ "pretty-ms": "^7.0.1", "qrcode": "^1.4.4", "rimraf": "^3.0.2", - "sass": "^1.34.1", + "sass": "^1.35.1", "tinymce": "^5.7.1", "typescript": "^4.2.4", "vite": "^2.3.7", diff --git a/app/src/components/v-upload/v-upload.vue b/app/src/components/v-upload/v-upload.vue index e53c9228f5..0903b96cda 100644 --- a/app/src/components/v-upload/v-upload.vue +++ b/app/src/components/v-upload/v-upload.vue @@ -274,7 +274,12 @@ export default defineComponent({ url: url.value, }); - emit('input', response.data.data); + if (props.multiple) { + emit('input', [response.data.data]); + } else { + emit('input', response.data.data); + } + activeDialog.value = null; url.value = ''; } catch (err) { diff --git a/app/src/displays/register.ts b/app/src/displays/register.ts index 92f643f5e3..50ead98a74 100644 --- a/app/src/displays/register.ts +++ b/app/src/displays/register.ts @@ -20,10 +20,12 @@ export async function registerDisplays(app: App): Promise { const result = await import(/* @vite-ignore */ `${getRootPath()}extensions/displays/${displayName}/index.js`); displays.push(result.default); } catch (err) { + // eslint-disable-next-line no-console console.warn(`Couldn't load custom displays "${displayName}":`, err); } }); } catch { + // eslint-disable-next-line no-console console.warn(`Couldn't load custom displays`); } diff --git a/app/src/interfaces/_system/system-collections/system-collections.vue b/app/src/interfaces/_system/system-collections/system-collections.vue index 423681435c..605f348f0c 100644 --- a/app/src/interfaces/_system/system-collections/system-collections.vue +++ b/app/src/interfaces/_system/system-collections/system-collections.vue @@ -5,7 +5,7 @@ diff --git a/app/src/interfaces/input-autocomplete-api/input-autocomplete-api.vue b/app/src/interfaces/input-autocomplete-api/input-autocomplete-api.vue index 4e4811abff..6270781c99 100644 --- a/app/src/interfaces/input-autocomplete-api/input-autocomplete-api.vue +++ b/app/src/interfaces/input-autocomplete-api/input-autocomplete-api.vue @@ -100,6 +100,7 @@ export default defineComponent({ const resultsArray = get(result.data, props.resultsPath); if (Array.isArray(resultsArray) === false) { + // eslint-disable-next-line no-console console.warn(`Expected results type of array, "${typeof resultsArray}" recieved`); return; } else { @@ -108,6 +109,7 @@ export default defineComponent({ .filter((val: unknown) => val); } } catch (err) { + // eslint-disable-next-line no-console console.warn(err); } }; diff --git a/app/src/interfaces/input-rich-text-html/input-rich-text-html.vue b/app/src/interfaces/input-rich-text-html/input-rich-text-html.vue index 1291329210..5c2a53ce3f 100644 --- a/app/src/interfaces/input-rich-text-html/input-rich-text-html.vue +++ b/app/src/interfaces/input-rich-text-html/input-rich-text-html.vue @@ -4,9 +4,8 @@ ref="editorElement" :init="editorOptions" :disabled="disabled" - model-events="change keydown blur focus paste ExecCommand SetContent" + model-events="change keydown blur focus paste ExecCommand" v-model="internalValue" - @change="onChange" @onFocusIn="setFocus(true)" @onFocusOut="setFocus(false)" /> @@ -242,6 +241,7 @@ export default defineComponent({ editorRef, imageToken ); + const { mediaDrawerOpen, mediaSelection, @@ -344,9 +344,6 @@ export default defineComponent({ closeCodeDrawer, saveCode, sourceCodeButton, - onChange(a: any) { - console.log(a); - }, }; function setup(editor: any) { @@ -374,9 +371,12 @@ export default defineComponent({ }); - + +