mirror of
https://github.com/directus/directus.git
synced 2026-01-31 22:18:00 -05:00
Merge pull request #139 from directus/feature-rate-limiting
Add rate limiter
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"useTabs": true
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"useTabs": true
|
||||
}
|
||||
|
||||
@@ -43,10 +43,9 @@ Pull requests are more than welcome and always appreciated. Seeing this is in ac
|
||||
|
||||
Directus is a GPLv3-licensed open source project with development made possible by support from our core team, contributors, and sponsors. It's not easy building premium open-source software; if you would like to help ensure Directus stays free, please consider becoming a sponsor.
|
||||
|
||||
* [Support us through GitHub Sponsors](https://github.com/sponsors/directus)
|
||||
* [One-time donation through PayPal](https://www.paypal.me/supportdirectus)
|
||||
- [Support us through GitHub Sponsors](https://github.com/sponsors/directus)
|
||||
- [One-time donation through PayPal](https://www.paypal.me/supportdirectus)
|
||||
|
||||
## 📄 License
|
||||
|
||||
Directus is released under [the GPLv3 license](./license). Monospace Inc. owns all Directus trademarks and logos on behalf of our project's community. Copyright © 2006-2020, Monospace Inc.
|
||||
|
||||
|
||||
@@ -19,6 +19,33 @@ DB_PASSWORD="psql1234"
|
||||
## SQLite Example
|
||||
# DB_FILENAME="./data.db"
|
||||
|
||||
####################################################################################################
|
||||
# Rate Limiting
|
||||
|
||||
RATE_LIMITER_ENABLED=true
|
||||
|
||||
RATE_LIMITER_POINTS=50
|
||||
RATE_LIMITER_DURATION=1
|
||||
|
||||
RATE_LIMITER_STORE=memory # memory | redis | memcache
|
||||
|
||||
## Redis (see https://github.com/animir/node-rate-limiter-flexible/wiki/Redis and
|
||||
## https://www.npmjs.com/package/ioredis#connect-to-redis)
|
||||
# RATE_LIMITER_EXEC_EVENLY=false
|
||||
# RATE_LIMITER_BLOCK_DURATION=0
|
||||
# RATE_LIMITER_KEY_PREFIX=rlflx
|
||||
|
||||
# RATE_LIMITER_REDIS="redis://:authpassword@127.0.0.1:6380/4"
|
||||
# --OR--
|
||||
# RATE_LIMITER_REDIS_HOST="127.0.0.1"
|
||||
# RATE_LIMITER_REDIS_PORT="127.0.0.1"
|
||||
# RATE_LIMITER_REDIS_PASSWORD="127.0.0.1"
|
||||
# RATE_LIMITER_REDIS_DB="127.0.0.1"
|
||||
|
||||
## Memcache (see https://github.com/animir/node-rate-limiter-flexible/wiki/Memcache and
|
||||
## https://www.npmjs.com/package/memcached)
|
||||
# RATE_LIMITER_MEMCACHE='localhost:11211'
|
||||
|
||||
####################################################################################################
|
||||
# File Storage
|
||||
|
||||
|
||||
40701
api/package-lock.json
generated
40701
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
329
api/package.json
329
api/package.json
@@ -1,165 +1,168 @@
|
||||
{
|
||||
"name": "directus",
|
||||
"version": "9.0.0-alpha.33",
|
||||
"license": "GPL-3.0-only",
|
||||
"homepage": "https://github.com/directus/next#readme",
|
||||
"description": "Directus is a real-time API and App dashboard for managing SQL database content.",
|
||||
"keywords": [
|
||||
"directus",
|
||||
"realtime",
|
||||
"database",
|
||||
"content",
|
||||
"api",
|
||||
"rest",
|
||||
"graphql",
|
||||
"app",
|
||||
"dashboard",
|
||||
"headless",
|
||||
"cms",
|
||||
"mysql",
|
||||
"postgresql",
|
||||
"sqlite",
|
||||
"framework",
|
||||
"vue"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/directus/next.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/directus/next/issues"
|
||||
},
|
||||
"author": {
|
||||
"name": "Monospace Inc",
|
||||
"email": "info@monospace.io",
|
||||
"url": "https://monospace.io"
|
||||
},
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Rijk van Zanten",
|
||||
"email": "rijkvanzanten@me.com",
|
||||
"url": "https://github.com/rijkvanzanten"
|
||||
},
|
||||
{
|
||||
"name": "Ben Haynes",
|
||||
"email": "ben@rngr.org",
|
||||
"url": "https://github.com/benhaynes"
|
||||
}
|
||||
],
|
||||
"main": "dist/app.js",
|
||||
"bin": {
|
||||
"directus": "dist/cli/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=production node dist/server.js",
|
||||
"build": "rm -rf dist && tsc -b && copyfiles \"src/**/*.*\" -e \"src/**/*.ts\" -u 1 dist",
|
||||
"dev": "cross-env NODE_ENV=development LOG_LEVEL=trace ts-node-dev --files src/server.ts --respawn --watch \"src/**/*.ts\" --transpile-only",
|
||||
"cli": "cross-env NODE_ENV=development ts-node --script-mode --transpile-only src/cli/index.ts",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"example.env"
|
||||
],
|
||||
"dependencies": {
|
||||
"@directus/app": "^9.0.0-alpha.33",
|
||||
"@directus/format-title": "^3.2.0",
|
||||
"@slynova/flydrive": "^1.0.2",
|
||||
"@slynova/flydrive-gcs": "^1.0.2",
|
||||
"@slynova/flydrive-s3": "^1.0.2",
|
||||
"argon2": "^0.26.2",
|
||||
"atob": "^2.1.2",
|
||||
"axios": "^0.19.2",
|
||||
"body-parser": "^1.19.0",
|
||||
"busboy": "^0.3.1",
|
||||
"camelcase": "^6.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
"commander": "^5.1.0",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^8.2.0",
|
||||
"execa": "^4.0.3",
|
||||
"exif-reader": "^1.0.3",
|
||||
"express": "^4.17.1",
|
||||
"express-async-handler": "^1.1.4",
|
||||
"express-pino-logger": "^5.0.0",
|
||||
"express-session": "^1.17.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
"grant": "^5.3.0",
|
||||
"icc": "^2.0.0",
|
||||
"inquirer": "^7.3.3",
|
||||
"joi": "^17.1.1",
|
||||
"js-yaml": "^3.14.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"knex": "^0.21.4",
|
||||
"knex-schema-inspector": "0.0.11",
|
||||
"liquidjs": "^9.14.1",
|
||||
"lodash": "^4.17.19",
|
||||
"macos-release": "^2.4.1",
|
||||
"ms": "^2.1.2",
|
||||
"nanoid": "^3.1.12",
|
||||
"nodemailer": "^6.4.11",
|
||||
"ora": "^4.1.1",
|
||||
"otplib": "^12.0.1",
|
||||
"pino": "^6.4.1",
|
||||
"pino-colada": "^2.1.0",
|
||||
"resolve-cwd": "^3.0.0",
|
||||
"sharp": "^0.25.4",
|
||||
"uuid": "^8.3.0",
|
||||
"uuid-validate": "0.0.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"mssql": "^6.2.0",
|
||||
"mysql": "^2.18.1",
|
||||
"oracledb": "^5.0.0",
|
||||
"pg": "^8.3.3",
|
||||
"sqlite3": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/atob": "^2.1.2",
|
||||
"@types/busboy": "^0.2.3",
|
||||
"@types/clear": "^0.1.0",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/cors": "^2.8.7",
|
||||
"@types/express": "^4.17.7",
|
||||
"@types/express-pino-logger": "^4.0.2",
|
||||
"@types/express-session": "^1.17.0",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/inquirer": "^6.5.0",
|
||||
"@types/joi": "^14.3.4",
|
||||
"@types/js-yaml": "^3.12.5",
|
||||
"@types/jsonwebtoken": "^8.5.0",
|
||||
"@types/lodash": "^4.14.159",
|
||||
"@types/ms": "^0.7.31",
|
||||
"@types/nodemailer": "^6.4.0",
|
||||
"@types/pino": "^6.3.0",
|
||||
"@types/sharp": "^0.25.1",
|
||||
"@types/uuid": "^8.0.0",
|
||||
"@types/uuid-validate": "0.0.1",
|
||||
"concat-map": "0.0.1",
|
||||
"copyfiles": "^2.3.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"eslint": "^7.6.0",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"husky": "^4.2.5",
|
||||
"lint-staged": "^10.2.11",
|
||||
"prettier": "^2.0.5",
|
||||
"ts-node": "^8.10.2",
|
||||
"ts-node-dev": "^1.0.0-pre.56",
|
||||
"tslint": "^6.1.3",
|
||||
"typescript": "^3.9.7"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,ts}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"gitHead": "a7d3952ec3b812ee1eec2f1c7b9f44186cbe0498"
|
||||
"name": "directus",
|
||||
"version": "9.0.0-alpha.33",
|
||||
"license": "GPL-3.0-only",
|
||||
"homepage": "https://github.com/directus/next#readme",
|
||||
"description": "Directus is a real-time API and App dashboard for managing SQL database content.",
|
||||
"keywords": [
|
||||
"directus",
|
||||
"realtime",
|
||||
"database",
|
||||
"content",
|
||||
"api",
|
||||
"rest",
|
||||
"graphql",
|
||||
"app",
|
||||
"dashboard",
|
||||
"headless",
|
||||
"cms",
|
||||
"mysql",
|
||||
"postgresql",
|
||||
"sqlite",
|
||||
"framework",
|
||||
"vue"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/directus/next.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/directus/next/issues"
|
||||
},
|
||||
"author": {
|
||||
"name": "Monospace Inc",
|
||||
"email": "info@monospace.io",
|
||||
"url": "https://monospace.io"
|
||||
},
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Rijk van Zanten",
|
||||
"email": "rijkvanzanten@me.com",
|
||||
"url": "https://github.com/rijkvanzanten"
|
||||
},
|
||||
{
|
||||
"name": "Ben Haynes",
|
||||
"email": "ben@rngr.org",
|
||||
"url": "https://github.com/benhaynes"
|
||||
}
|
||||
],
|
||||
"main": "dist/app.js",
|
||||
"bin": {
|
||||
"directus": "dist/cli/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=production node dist/server.js",
|
||||
"build": "rm -rf dist && tsc -b && copyfiles \"src/**/*.*\" -e \"src/**/*.ts\" -u 1 dist",
|
||||
"dev": "cross-env NODE_ENV=development LOG_LEVEL=trace ts-node-dev --files src/server.ts --respawn --watch \"src/**/*.ts\" --transpile-only",
|
||||
"cli": "cross-env NODE_ENV=development ts-node --script-mode --transpile-only src/cli/index.ts",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"example.env"
|
||||
],
|
||||
"dependencies": {
|
||||
"@directus/app": "^9.0.0-alpha.33",
|
||||
"@directus/format-title": "^3.2.0",
|
||||
"@slynova/flydrive": "^1.0.2",
|
||||
"@slynova/flydrive-gcs": "^1.0.2",
|
||||
"@slynova/flydrive-s3": "^1.0.2",
|
||||
"argon2": "^0.26.2",
|
||||
"atob": "^2.1.2",
|
||||
"axios": "^0.19.2",
|
||||
"body-parser": "^1.19.0",
|
||||
"busboy": "^0.3.1",
|
||||
"camelcase": "^6.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
"commander": "^5.1.0",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^8.2.0",
|
||||
"execa": "^4.0.3",
|
||||
"exif-reader": "^1.0.3",
|
||||
"express": "^4.17.1",
|
||||
"express-async-handler": "^1.1.4",
|
||||
"express-pino-logger": "^5.0.0",
|
||||
"express-session": "^1.17.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
"grant": "^5.3.0",
|
||||
"icc": "^2.0.0",
|
||||
"inquirer": "^7.3.3",
|
||||
"joi": "^17.1.1",
|
||||
"js-yaml": "^3.14.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"knex": "^0.21.4",
|
||||
"knex-schema-inspector": "0.0.11",
|
||||
"liquidjs": "^9.14.1",
|
||||
"lodash": "^4.17.19",
|
||||
"macos-release": "^2.4.1",
|
||||
"ms": "^2.1.2",
|
||||
"nanoid": "^3.1.12",
|
||||
"nodemailer": "^6.4.11",
|
||||
"ora": "^4.1.1",
|
||||
"otplib": "^12.0.1",
|
||||
"pino": "^6.4.1",
|
||||
"pino-colada": "^2.1.0",
|
||||
"rate-limiter-flexible": "^2.1.10",
|
||||
"resolve-cwd": "^3.0.0",
|
||||
"sharp": "^0.25.4",
|
||||
"uuid": "^8.3.0",
|
||||
"uuid-validate": "0.0.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"ioredis": "^4.17.3",
|
||||
"memcached": "^2.2.2",
|
||||
"mssql": "^6.2.0",
|
||||
"mysql": "^2.18.1",
|
||||
"oracledb": "^5.0.0",
|
||||
"pg": "^8.3.3",
|
||||
"sqlite3": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/atob": "^2.1.2",
|
||||
"@types/busboy": "^0.2.3",
|
||||
"@types/clear": "^0.1.0",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/cors": "^2.8.7",
|
||||
"@types/express": "^4.17.7",
|
||||
"@types/express-pino-logger": "^4.0.2",
|
||||
"@types/express-session": "^1.17.0",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/inquirer": "^6.5.0",
|
||||
"@types/joi": "^14.3.4",
|
||||
"@types/js-yaml": "^3.12.5",
|
||||
"@types/jsonwebtoken": "^8.5.0",
|
||||
"@types/lodash": "^4.14.159",
|
||||
"@types/ms": "^0.7.31",
|
||||
"@types/nodemailer": "^6.4.0",
|
||||
"@types/pino": "^6.3.0",
|
||||
"@types/sharp": "^0.25.1",
|
||||
"@types/uuid": "^8.0.0",
|
||||
"@types/uuid-validate": "0.0.1",
|
||||
"concat-map": "0.0.1",
|
||||
"copyfiles": "^2.3.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"eslint": "^7.6.0",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"husky": "^4.2.5",
|
||||
"lint-staged": "^10.2.11",
|
||||
"prettier": "^2.0.5",
|
||||
"ts-node": "^8.10.2",
|
||||
"ts-node-dev": "^1.0.0-pre.56",
|
||||
"tslint": "^6.1.3",
|
||||
"typescript": "^3.9.7"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npx lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,ts}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"gitHead": "a7d3952ec3b812ee1eec2f1c7b9f44186cbe0498"
|
||||
}
|
||||
|
||||
@@ -3,14 +3,15 @@ import bodyParser from 'body-parser';
|
||||
import logger from './logger';
|
||||
import expressLogger from 'express-pino-logger';
|
||||
import path from 'path';
|
||||
import cors from 'cors';
|
||||
|
||||
import { validateEnv } from './utils/validate-env';
|
||||
import env from './env';
|
||||
|
||||
import errorHandler from './middleware/error-handler';
|
||||
|
||||
import cors from './middleware/cors';
|
||||
import extractToken from './middleware/extract-token';
|
||||
import authenticate from './middleware/authenticate';
|
||||
|
||||
import rateLimiter from './middleware/rate-limiter';
|
||||
import activityRouter from './controllers/activity';
|
||||
import assetsRouter from './controllers/assets';
|
||||
import authRouter from './controllers/auth';
|
||||
@@ -33,63 +34,62 @@ import webhooksRouter from './controllers/webhooks';
|
||||
|
||||
import notFoundHandler from './controllers/not-found';
|
||||
|
||||
const app = express().disable('x-powered-by').set('trust proxy', true);
|
||||
validateEnv(['KEY', 'SECRET']);
|
||||
|
||||
app.use(expressLogger({ logger }))
|
||||
.use(bodyParser.json())
|
||||
.use(extractToken)
|
||||
.use((req, res, next) => {
|
||||
res.setHeader('X-Powered-By', 'Directus');
|
||||
next();
|
||||
});
|
||||
const app = express();
|
||||
app.disable('x-powered-by');
|
||||
app.set('trust proxy', true);
|
||||
|
||||
if (env.CORS_ENABLED === 'true') {
|
||||
app.use(
|
||||
cors({
|
||||
origin: env.CORS_ORIGIN || true,
|
||||
methods: env.CORS_METHODS || 'GET,POST,PATCH,DELETE',
|
||||
allowedHeaders: env.CORS_ALLOWED_HEADERS,
|
||||
exposedHeaders: env.CORS_EXPOSED_HEADERS,
|
||||
credentials: env.CORS_CREDENTIALS === 'true' || undefined,
|
||||
maxAge: Number(env.CORS_MAX_AGE) || undefined,
|
||||
})
|
||||
);
|
||||
app.use(expressLogger({ logger }));
|
||||
app.use(bodyParser.json());
|
||||
app.use(extractToken);
|
||||
app.use((req, res, next) => {
|
||||
res.setHeader('X-Powered-By', 'Directus');
|
||||
next();
|
||||
});
|
||||
|
||||
if (env.CORS_ENABLED === true) {
|
||||
app.use(cors);
|
||||
}
|
||||
|
||||
if (env.NODE_ENV !== 'development') {
|
||||
const adminPath = require.resolve('@directus/app/dist/index.html');
|
||||
|
||||
app.get('/', (req, res) => res.redirect('/admin/'))
|
||||
// the auth endpoints allow you to login/logout etc. It should ignore the authentication check
|
||||
.use('/admin', express.static(path.join(adminPath, '..')))
|
||||
.use('/admin/*', (req, res) => {
|
||||
res.sendFile(adminPath);
|
||||
});
|
||||
app.get('/', (req, res) => res.redirect('/admin/'));
|
||||
app.use('/admin', express.static(path.join(adminPath, '..')));
|
||||
app.use('/admin/*', (req, res) => {
|
||||
res.sendFile(adminPath);
|
||||
});
|
||||
}
|
||||
|
||||
app.use('/auth', authRouter)
|
||||
// use the rate limiter - all routes for now
|
||||
if (env.RATE_LIMITER_ENABLED === true) {
|
||||
app.use(rateLimiter);
|
||||
}
|
||||
|
||||
.use(authenticate)
|
||||
app.use('/auth', authRouter);
|
||||
app.use(authenticate);
|
||||
|
||||
.use('/activity', activityRouter)
|
||||
.use('/assets', assetsRouter)
|
||||
.use('/collections', collectionsRouter)
|
||||
.use('/extensions', extensionsRouter)
|
||||
.use('/fields', fieldsRouter)
|
||||
.use('/files', filesRouter)
|
||||
.use('/folders', foldersRouter)
|
||||
.use('/items', itemsRouter)
|
||||
.use('/permissions', permissionsRouter)
|
||||
.use('/presets', presetsRouter)
|
||||
.use('/relations', relationsRouter)
|
||||
.use('/revisions', revisionsRouter)
|
||||
.use('/roles', rolesRouter)
|
||||
.use('/server/', serverRouter)
|
||||
.use('/settings', settingsRouter)
|
||||
.use('/users', usersRouter)
|
||||
.use('/utils', utilsRouter)
|
||||
.use('/webhooks', webhooksRouter);
|
||||
app.use('/activity', activityRouter);
|
||||
app.use('/assets', assetsRouter);
|
||||
app.use('/collections', collectionsRouter);
|
||||
app.use('/extensions', extensionsRouter);
|
||||
app.use('/fields', fieldsRouter);
|
||||
app.use('/files', filesRouter);
|
||||
app.use('/folders', foldersRouter);
|
||||
app.use('/items', itemsRouter);
|
||||
app.use('/permissions', permissionsRouter);
|
||||
app.use('/presets', presetsRouter);
|
||||
app.use('/relations', relationsRouter);
|
||||
app.use('/revisions', revisionsRouter);
|
||||
app.use('/roles', rolesRouter);
|
||||
app.use('/server/', serverRouter);
|
||||
app.use('/settings', settingsRouter);
|
||||
app.use('/users', usersRouter);
|
||||
app.use('/utils', utilsRouter);
|
||||
app.use('/webhooks', webhooksRouter);
|
||||
|
||||
app.use(notFoundHandler).use(errorHandler);
|
||||
app.use(notFoundHandler);
|
||||
app.use(errorHandler);
|
||||
|
||||
export default app;
|
||||
|
||||
@@ -50,10 +50,17 @@ const password = () => ({
|
||||
mask: '*',
|
||||
});
|
||||
|
||||
const ssl = () => ({
|
||||
type: 'confirm',
|
||||
name: 'ssl',
|
||||
message: 'Enable SSL:',
|
||||
default: false,
|
||||
});
|
||||
|
||||
export const databaseQuestions = {
|
||||
sqlite3: [filename],
|
||||
mysql: [host, port, database, user, password],
|
||||
pg: [host, port, database, user, password],
|
||||
pg: [host, port, database, user, password, ssl],
|
||||
oracledb: [host, port, database, user, password],
|
||||
mssql: [host, port, database, user, password],
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ export type Credentials = {
|
||||
database?: string;
|
||||
user?: string;
|
||||
password?: string;
|
||||
ssl?: boolean;
|
||||
};
|
||||
export default function createDBConnection(
|
||||
client: 'sqlite3' | 'mysql' | 'pg' | 'oracledb' | 'mssql',
|
||||
@@ -22,15 +23,28 @@ export default function createDBConnection(
|
||||
filename: filename as string,
|
||||
};
|
||||
} else {
|
||||
const { host, port, database, user, password } = credentials as Credentials;
|
||||
if (client !== 'pg') {
|
||||
const { host, port, database, user, password } = credentials as Credentials;
|
||||
|
||||
connection = {
|
||||
host: host,
|
||||
port: port,
|
||||
database: database,
|
||||
user: user,
|
||||
password: password,
|
||||
};
|
||||
connection = {
|
||||
host: host,
|
||||
port: port,
|
||||
database: database,
|
||||
user: user,
|
||||
password: password,
|
||||
};
|
||||
} else {
|
||||
const { host, port, database, user, password, ssl } = credentials as Credentials;
|
||||
|
||||
connection = {
|
||||
host: host,
|
||||
port: port,
|
||||
database: database,
|
||||
user: user,
|
||||
password: password,
|
||||
ssl: ssl,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const knexConfig: Config = {
|
||||
|
||||
@@ -1,34 +1,54 @@
|
||||
####################################################################################################
|
||||
# General
|
||||
## General
|
||||
|
||||
{{ general }}
|
||||
PORT=41201
|
||||
PUBLIC_URL="/"
|
||||
|
||||
####################################################################################################
|
||||
# Database
|
||||
## Database
|
||||
|
||||
{{ database }}
|
||||
|
||||
####################################################################################################
|
||||
# File Storage
|
||||
## Rate Limiting
|
||||
|
||||
{{ storage }}
|
||||
RATE_LIMITER_ENABLED=true
|
||||
RATE_LIMITER_STORE=memory
|
||||
RATE_LIMITER_POINTS=25
|
||||
RATE_LIMITER_DURATION=1
|
||||
|
||||
####################################################################################################
|
||||
# Security
|
||||
## File Storage
|
||||
|
||||
STORAGE_LOCATIONS="local"
|
||||
STORAGE_LOCAL_PUBLIC_URL="/uploads"
|
||||
STORAGE_LOCAL_DRIVER="local"
|
||||
STORAGE_LOCAL_ROOT="./uploads"
|
||||
|
||||
####################################################################################################
|
||||
## Security
|
||||
|
||||
{{ security }}
|
||||
|
||||
####################################################################################################
|
||||
# SSO (OAuth) Providers
|
||||
|
||||
{{ oauth }}
|
||||
ACCESS_TOKEN_TTL="15m",
|
||||
REFRESH_TOKEN_TTL="7d",
|
||||
REFRESH_TOKEN_COOKIE_SECURE=false,
|
||||
REFRESH_TOKEN_COOKIE_SAME_SITE="lax",
|
||||
|
||||
####################################################################################################
|
||||
# Extensions
|
||||
## SSO (OAuth) Providers
|
||||
|
||||
{{ extensions }}
|
||||
OAUTH_PROVIDERS=""
|
||||
|
||||
####################################################################################################
|
||||
# Email
|
||||
## Extensions
|
||||
|
||||
{{ email }}
|
||||
EXTENSIONS_PATH="./extensions"
|
||||
|
||||
####################################################################################################
|
||||
## Email
|
||||
|
||||
EMAIL_FROM="no-reply@directus.io"
|
||||
EMAIL_TRANSPORT="sendmail"
|
||||
EMAIL_SENDMAIL_NEW_LINE="unix"
|
||||
EMAIL_SENDMAIL_PATH="/usr/sbin/sendmail"
|
||||
|
||||
@@ -15,35 +15,9 @@ const liquidEngine = new Liquid({
|
||||
});
|
||||
|
||||
const defaults = {
|
||||
general: {
|
||||
PORT: 41201,
|
||||
PUBLIC_URL: '/',
|
||||
},
|
||||
storage: {
|
||||
STORAGE_LOCATIONS: 'local',
|
||||
STORAGE_LOCAL_PUBLIC_URL: '/uploads',
|
||||
STORAGE_LOCAL_DRIVER: 'local',
|
||||
STORAGE_LOCAL_ROOT: './uploads',
|
||||
},
|
||||
security: {
|
||||
KEY: uuidv4(),
|
||||
SECRET: nanoid(32),
|
||||
ACCESS_TOKEN_TTL: '15m',
|
||||
REFRESH_TOKEN_TTL: '7d',
|
||||
REFRESH_TOKEN_COOKIE_SECURE: false,
|
||||
REFRESH_TOKEN_COOKIE_SAME_SITE: 'lax',
|
||||
},
|
||||
oauth: {
|
||||
OAUTH_PROVIDERS: '',
|
||||
},
|
||||
extensions: {
|
||||
EXTENSIONS_PATH: './extensions',
|
||||
},
|
||||
email: {
|
||||
EMAIL_FROM: 'no-reply@directus.io',
|
||||
EMAIL_TRANSPORT: 'sendmail',
|
||||
EMAIL_SENDMAIL_NEW_LINE: 'unix',
|
||||
EMAIL_SENDMAIL_PATH: '/usr/sbin/sendmail',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const loginSchema = Joi.object({
|
||||
email: Joi.string().email().required(),
|
||||
password: Joi.string().required(),
|
||||
mode: Joi.string().valid('cookie', 'json'),
|
||||
otp: Joi.string()
|
||||
otp: Joi.string(),
|
||||
});
|
||||
|
||||
router.post(
|
||||
@@ -178,7 +178,7 @@ router.post(
|
||||
return res.status(200).end();
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/password/reset',
|
||||
@@ -201,7 +201,7 @@ router.post(
|
||||
await service.resetPassword(req.body.token, req.body.password);
|
||||
return res.status(200).end();
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
router.use(
|
||||
'/sso',
|
||||
|
||||
@@ -67,9 +67,17 @@ const multipartHandler = asyncHandler(async (req, res, next) => {
|
||||
storage: payload.storage || disk,
|
||||
};
|
||||
|
||||
const primaryKey = await service.upload(fileStream, payloadWithRequiredFields, existingPrimaryKey);
|
||||
savedFiles.push(primaryKey);
|
||||
tryDone();
|
||||
try {
|
||||
const primaryKey = await service.upload(
|
||||
fileStream,
|
||||
payloadWithRequiredFields,
|
||||
existingPrimaryKey
|
||||
);
|
||||
savedFiles.push(primaryKey);
|
||||
tryDone();
|
||||
} catch (error) {
|
||||
busboy.emit('error', error);
|
||||
}
|
||||
});
|
||||
|
||||
busboy.on('error', (error: Error) => {
|
||||
@@ -112,7 +120,7 @@ router.post(
|
||||
);
|
||||
|
||||
const importSchema = Joi.object({
|
||||
url: Joi.string().required()
|
||||
url: Joi.string().required(),
|
||||
});
|
||||
|
||||
router.post(
|
||||
@@ -128,7 +136,7 @@ router.post(
|
||||
const service = new FilesService({ accountability: req.accountability });
|
||||
|
||||
const fileResponse = await axios.get<NodeJS.ReadableStream>(req.body.url, {
|
||||
responseType: 'stream'
|
||||
responseType: 'stream',
|
||||
});
|
||||
|
||||
const parsedURL = url.parse(fileResponse.request.res.responseUrl);
|
||||
@@ -139,7 +147,7 @@ router.post(
|
||||
storage: (env.STORAGE_LOCATIONS as string).split(',')[0].trim(),
|
||||
type: fileResponse.headers['content-type'],
|
||||
title: formatTitle(filename),
|
||||
...req.body
|
||||
...req.body,
|
||||
};
|
||||
|
||||
delete payload.url;
|
||||
@@ -148,7 +156,7 @@ router.post(
|
||||
const record = await service.readByKey(primaryKey, req.sanitizedQuery);
|
||||
return res.json({ data: record || null });
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
|
||||
@@ -49,15 +49,15 @@ router.get(
|
||||
query.filter = {
|
||||
...(query.filter || {}),
|
||||
role: {
|
||||
_eq: req.accountability.role
|
||||
}
|
||||
}
|
||||
_eq: req.accountability.role,
|
||||
},
|
||||
};
|
||||
|
||||
const items = await service.readByQuery(req.sanitizedQuery);
|
||||
|
||||
return res.json({ data: items || null });
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:pk',
|
||||
|
||||
@@ -153,38 +153,44 @@ router.post(
|
||||
})
|
||||
);
|
||||
|
||||
router.post('/me/tfa/enable/', asyncHandler(async (req, res) => {
|
||||
if (!req.accountability?.user) {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
router.post(
|
||||
'/me/tfa/enable/',
|
||||
asyncHandler(async (req, res) => {
|
||||
if (!req.accountability?.user) {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
const service = new UsersService({ accountability: req.accountability });
|
||||
const { url, secret } = await service.enableTFA(req.accountability.user);
|
||||
const service = new UsersService({ accountability: req.accountability });
|
||||
const { url, secret } = await service.enableTFA(req.accountability.user);
|
||||
|
||||
return res.json({ data: { secret, otpauth_url: url }});
|
||||
}));
|
||||
return res.json({ data: { secret, otpauth_url: url } });
|
||||
})
|
||||
);
|
||||
|
||||
router.post('/me/tfa/disable', asyncHandler(async (req, res) => {
|
||||
if (!req.accountability?.user) {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
router.post(
|
||||
'/me/tfa/disable',
|
||||
asyncHandler(async (req, res) => {
|
||||
if (!req.accountability?.user) {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
if (!req.body.otp) {
|
||||
throw new InvalidPayloadException(`"otp" is required`);
|
||||
}
|
||||
if (!req.body.otp) {
|
||||
throw new InvalidPayloadException(`"otp" is required`);
|
||||
}
|
||||
|
||||
const service = new UsersService({ accountability: req.accountability });
|
||||
const authService = new AuthService({ accountability: req.accountability });
|
||||
const service = new UsersService({ accountability: req.accountability });
|
||||
const authService = new AuthService({ accountability: req.accountability });
|
||||
|
||||
const otpValid = await authService.verifyOTP(req.accountability.user, req.body.otp);
|
||||
const otpValid = await authService.verifyOTP(req.accountability.user, req.body.otp);
|
||||
|
||||
if (otpValid === false) {
|
||||
throw new InvalidPayloadException(`"otp" is invalid`);
|
||||
}
|
||||
if (otpValid === false) {
|
||||
throw new InvalidPayloadException(`"otp" is invalid`);
|
||||
}
|
||||
|
||||
await service.disableTFA(req.accountability.user);
|
||||
await service.disableTFA(req.accountability.user);
|
||||
|
||||
return res.status(200).end();
|
||||
}));
|
||||
return res.status(200).end();
|
||||
})
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -68,6 +68,6 @@ router.post(
|
||||
|
||||
return res.status(200).end();
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,10 @@
|
||||
/**
|
||||
* @NOTE
|
||||
* See example.env for all possible keys
|
||||
*/
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
import logger from './logger';
|
||||
import { clone } from 'lodash';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -12,6 +17,11 @@ const defaults: Record<string, any> = {
|
||||
STORAGE_LOCAL_DRIVER: 'local',
|
||||
STORAGE_LOCAL_ROOT: './uploads',
|
||||
|
||||
RATE_LIMITER_ENABLED: true,
|
||||
RATE_LIMITER_POINTS: 25,
|
||||
RATE_LIMITER_DURATION: 1,
|
||||
RATE_LIMITER_STORE: 'memory',
|
||||
|
||||
ACCESS_TOKEN_TTL: '15m',
|
||||
REFRESH_TOKEN_TTL: '7d',
|
||||
REFRESH_TOKEN_COOKIE_SECURE: false,
|
||||
@@ -29,31 +39,23 @@ const defaults: Record<string, any> = {
|
||||
EMAIL_SENDMAIL_PATH: '/usr/sbin/sendmail',
|
||||
};
|
||||
|
||||
const env: Record<string, any> = {
|
||||
let env: Record<string, any> = {
|
||||
...defaults,
|
||||
...process.env,
|
||||
};
|
||||
|
||||
env = processValues(env);
|
||||
|
||||
export default env;
|
||||
|
||||
export function validateEnv() {
|
||||
const requiredKeys = ['DB_CLIENT', 'KEY', 'SECRET'];
|
||||
function processValues(env: Record<string, any>) {
|
||||
env = clone(env);
|
||||
|
||||
if (env.DB_CLIENT && env.DB_CLIENT === 'sqlite3') {
|
||||
requiredKeys.push('DB_FILENAME');
|
||||
} else {
|
||||
requiredKeys.push('DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USER', 'DB_PASSWORD');
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
if (value === 'true') env[key] = true;
|
||||
if (value === 'false') env[key] = false;
|
||||
if (value === 'null') env[key] = null;
|
||||
}
|
||||
|
||||
for (const requiredKey of requiredKeys) {
|
||||
if (env.hasOwnProperty(requiredKey) === false) {
|
||||
logger.fatal(`Environment is missing the ${requiredKey} key.`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NOTE
|
||||
* See example.env for all possible keys
|
||||
*/
|
||||
|
||||
@@ -6,7 +6,7 @@ type Extensions = {
|
||||
collection?: string;
|
||||
item?: string | number | (string | number)[];
|
||||
action?: Permission['action'];
|
||||
}
|
||||
};
|
||||
|
||||
export class ForbiddenException extends BaseException {
|
||||
constructor(message = `You don't have permission to access this.`, extensions?: Extensions) {
|
||||
|
||||
12
api/src/exceptions/hit-rate-limit.ts
Normal file
12
api/src/exceptions/hit-rate-limit.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { BaseException } from './base';
|
||||
|
||||
type Extensions = {
|
||||
limit: number;
|
||||
reset: Date;
|
||||
};
|
||||
|
||||
export class HitRateLimitException extends BaseException {
|
||||
constructor(message: string, extensions: Extensions) {
|
||||
super(message, 429, 'REQUESTS_EXCEEDED', extensions);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ export * from './base';
|
||||
export * from './collection-not-found';
|
||||
export * from './field-not-found';
|
||||
export * from './forbidden';
|
||||
export * from './hit-rate-limit';
|
||||
export * from './invalid-credentials';
|
||||
export * from './invalid-otp';
|
||||
export * from './invalid-payload';
|
||||
@@ -9,3 +10,4 @@ export * from './invalid-query';
|
||||
export * from './item-limit';
|
||||
export * from './item-not-found';
|
||||
export * from './route-not-found';
|
||||
export * from './service-unavailable';
|
||||
|
||||
12
api/src/exceptions/service-unavailable.ts
Normal file
12
api/src/exceptions/service-unavailable.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { BaseException } from './base';
|
||||
|
||||
type Extensions = {
|
||||
service: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export class ServiceUnavailableException extends BaseException {
|
||||
constructor(message: string, extensions: Extensions) {
|
||||
super(message, 503, 'SERVICE_UNAVAILABLE', extensions);
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@ import pino, { LoggerOptions } from 'pino';
|
||||
const pinoOptions: LoggerOptions = { level: process.env.LOG_LEVEL || 'info' };
|
||||
|
||||
if (process.env.LOG_STYLE !== 'raw') {
|
||||
pinoOptions.prettyPrint = true;
|
||||
pinoOptions.prettifier = require('pino-colada');
|
||||
pinoOptions.prettyPrint = true;
|
||||
pinoOptions.prettifier = require('pino-colada');
|
||||
}
|
||||
|
||||
const logger = pino(pinoOptions);
|
||||
|
||||
18
api/src/middleware/cors.ts
Normal file
18
api/src/middleware/cors.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import cors from 'cors';
|
||||
import { RequestHandler } from 'express';
|
||||
import env from '../env';
|
||||
|
||||
let corsMiddleware: RequestHandler = (req, res, next) => next();
|
||||
|
||||
if (env.CORS_ENABLED === true) {
|
||||
corsMiddleware = cors({
|
||||
origin: env.CORS_ORIGIN || true,
|
||||
methods: env.CORS_METHODS || 'GET,POST,PATCH,DELETE',
|
||||
allowedHeaders: env.CORS_ALLOWED_HEADERS,
|
||||
exposedHeaders: env.CORS_EXPOSED_HEADERS,
|
||||
credentials: env.CORS_CREDENTIALS || undefined,
|
||||
maxAge: env.CORS_MAX_AGE || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export default corsMiddleware;
|
||||
@@ -18,8 +18,8 @@ const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
|
||||
extensions: {
|
||||
...err.extensions,
|
||||
code: err.code,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
@@ -33,16 +33,16 @@ const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
|
||||
message: err.message,
|
||||
extensions: {
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (env.NODE_ENV === 'development') {
|
||||
payload.errors[0].extensions.exception = {
|
||||
stack: err.stack
|
||||
}
|
||||
stack: err.stack,
|
||||
};
|
||||
}
|
||||
|
||||
return res.json(payload);
|
||||
|
||||
84
api/src/middleware/rate-limiter.ts
Normal file
84
api/src/middleware/rate-limiter.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { RequestHandler } from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import {
|
||||
RateLimiterMemory,
|
||||
RateLimiterRedis,
|
||||
RateLimiterMemcache,
|
||||
IRateLimiterOptions,
|
||||
IRateLimiterStoreOptions,
|
||||
RateLimiterStoreAbstract,
|
||||
} from 'rate-limiter-flexible';
|
||||
import env from '../env';
|
||||
import { getConfigFromEnv } from '../utils/get-config-from-env';
|
||||
import { HitRateLimitException } from '../exceptions';
|
||||
import ms from 'ms';
|
||||
import { validateEnv } from '../utils/validate-env';
|
||||
|
||||
let checkRateLimit: RequestHandler = (req, res, next) => next();
|
||||
|
||||
if (env.RATE_LIMITER_ENABLED === true) {
|
||||
validateEnv(['RATE_LIMITER_STORE', 'RATE_LIMITER_DURATION', 'RATE_LIMITER_POINTS']);
|
||||
|
||||
const rateLimiter = getRateLimiter();
|
||||
|
||||
checkRateLimit = asyncHandler(async (req, res, next) => {
|
||||
try {
|
||||
await rateLimiter.consume(req.ip, 1);
|
||||
} catch (rateLimiterRes) {
|
||||
if (rateLimiterRes instanceof Error) throw rateLimiterRes;
|
||||
|
||||
res.set('Retry-After', String(rateLimiterRes.msBeforeNext / 1000));
|
||||
throw new HitRateLimitException(
|
||||
`Too many requests, retry after ${ms(rateLimiterRes.msBeforeNext)}.`,
|
||||
{
|
||||
limit: +env.RATE_LIMITER_POINTS,
|
||||
reset: new Date(Date.now() + rateLimiterRes.msBeforeNext),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
export default checkRateLimit;
|
||||
|
||||
function getRateLimiter() {
|
||||
switch (env.RATE_LIMITER_STORE) {
|
||||
case 'redis':
|
||||
return new RateLimiterRedis(getConfig('redis'));
|
||||
case 'memcache':
|
||||
return new RateLimiterMemcache(getConfig('memcache'));
|
||||
case 'memory':
|
||||
default:
|
||||
return new RateLimiterMemory(getConfig());
|
||||
}
|
||||
}
|
||||
|
||||
function getConfig(store?: 'memory'): IRateLimiterOptions;
|
||||
function getConfig(store: 'redis' | 'memcache'): IRateLimiterStoreOptions;
|
||||
function getConfig(
|
||||
store: 'memory' | 'redis' | 'memcache' = 'memory'
|
||||
): IRateLimiterOptions | IRateLimiterStoreOptions {
|
||||
const config: any = getConfigFromEnv('RATE_LIMITER_', `RATE_LIMITER_${store}_`);
|
||||
|
||||
if (store === 'redis') {
|
||||
const Redis = require('ioredis');
|
||||
config.storeClient = new Redis(
|
||||
env.RATE_LIMITER_REDIS || getConfigFromEnv('RATE_LIMITER_REDIS_')
|
||||
);
|
||||
}
|
||||
|
||||
if (store === 'memcache') {
|
||||
const Memcached = require('memcached');
|
||||
config.storeClient = new Memcached(
|
||||
env.RATE_LIMITER_MEMCACHE,
|
||||
getConfigFromEnv('RATE_LIMITER_MEMCACHE_')
|
||||
);
|
||||
}
|
||||
|
||||
delete config.enabled;
|
||||
delete config.store;
|
||||
|
||||
return config;
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import app from './app';
|
||||
import logger from './logger';
|
||||
import env, { validateEnv } from './env';
|
||||
import env from './env';
|
||||
import { validateDBConnection } from './database';
|
||||
|
||||
export default async function start() {
|
||||
validateEnv();
|
||||
await validateDBConnection();
|
||||
|
||||
const port = env.NODE_ENV === 'development' ? 41201 : env.PORT;
|
||||
|
||||
@@ -3,7 +3,11 @@ import jwt from 'jsonwebtoken';
|
||||
import argon2 from 'argon2';
|
||||
import { nanoid } from 'nanoid';
|
||||
import ms from 'ms';
|
||||
import { InvalidCredentialsException, InvalidPayloadException, InvalidOTPException } from '../exceptions';
|
||||
import {
|
||||
InvalidCredentialsException,
|
||||
InvalidPayloadException,
|
||||
InvalidOTPException,
|
||||
} from '../exceptions';
|
||||
import { Session, Accountability, AbstractServiceOptions, Action } from '../types';
|
||||
import Knex from 'knex';
|
||||
import ActivityService from '../services/activity';
|
||||
@@ -111,7 +115,11 @@ export default class AuthenticationService {
|
||||
}
|
||||
|
||||
const record = await database
|
||||
.select<Session & { email: string, id: string }>('directus_sessions.*', 'directus_users.email', 'directus_users.id')
|
||||
.select<Session & { email: string; id: string }>(
|
||||
'directus_sessions.*',
|
||||
'directus_users.email',
|
||||
'directus_users.id'
|
||||
)
|
||||
.from('directus_sessions')
|
||||
.where({ 'directus_sessions.token': refreshToken })
|
||||
.leftJoin('directus_users', 'directus_sessions.user', 'directus_users.id')
|
||||
@@ -128,7 +136,9 @@ export default class AuthenticationService {
|
||||
const newRefreshToken = nanoid(64);
|
||||
const refreshTokenExpiration = new Date(Date.now() + ms(env.REFRESH_TOKEN_TTL as string));
|
||||
|
||||
await this.knex('directus_sessions').update({ token: newRefreshToken, expires: refreshTokenExpiration }).where({ token: refreshToken });
|
||||
await this.knex('directus_sessions')
|
||||
.update({ token: newRefreshToken, expires: refreshTokenExpiration })
|
||||
.where({ token: refreshToken });
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
@@ -148,13 +158,21 @@ export default class AuthenticationService {
|
||||
}
|
||||
|
||||
async generateOTPAuthURL(pk: string, secret: string) {
|
||||
const user = await this.knex.select('first_name', 'last_name').from('directus_users').where({ id: pk }).first();
|
||||
const user = await this.knex
|
||||
.select('first_name', 'last_name')
|
||||
.from('directus_users')
|
||||
.where({ id: pk })
|
||||
.first();
|
||||
const name = `${user.first_name} ${user.last_name}`;
|
||||
return authenticator.keyuri(name, 'Directus', secret);
|
||||
}
|
||||
|
||||
async verifyOTP(pk: string, otp: string): Promise<boolean> {
|
||||
const user = await this.knex.select('tfa_secret').from('directus_users').where({ id: pk }).first();
|
||||
const user = await this.knex
|
||||
.select('tfa_secret')
|
||||
.from('directus_users')
|
||||
.where({ id: pk })
|
||||
.first();
|
||||
|
||||
if (!user.tfa_secret) {
|
||||
throw new InvalidPayloadException(`User "${pk}" doesn't have TFA enabled.`);
|
||||
|
||||
@@ -127,7 +127,7 @@ export default class AuthorizationService {
|
||||
|
||||
function applyFilters(
|
||||
ast: AST | NestedCollectionAST | FieldAST,
|
||||
accountability: Accountability | null,
|
||||
accountability: Accountability | null
|
||||
): AST | NestedCollectionAST | FieldAST {
|
||||
if (ast.type === 'collection') {
|
||||
const collection = ast.name;
|
||||
@@ -142,10 +142,7 @@ export default class AuthorizationService {
|
||||
ast.query = {
|
||||
...ast.query,
|
||||
filter: {
|
||||
_and: [
|
||||
(ast.query.filter || {}),
|
||||
parsedPermissions,
|
||||
]
|
||||
_and: [ast.query.filter || {}, parsedPermissions],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -160,7 +157,10 @@ export default class AuthorizationService {
|
||||
ast.query.limit = permissions.limit;
|
||||
}
|
||||
|
||||
ast.children = ast.children.map(child => applyFilters(child, accountability)) as (NestedCollectionAST | FieldAST)[];
|
||||
ast.children = ast.children.map((child) => applyFilters(child, accountability)) as (
|
||||
| NestedCollectionAST
|
||||
| FieldAST
|
||||
)[];
|
||||
}
|
||||
|
||||
return ast;
|
||||
@@ -207,7 +207,7 @@ export default class AuthorizationService {
|
||||
if (invalidKeys.length > 0) {
|
||||
throw new ForbiddenException(
|
||||
`You're not allowed to ${action} field "${invalidKeys[0]}" in collection "${collection}".`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,7 +233,11 @@ export default class AuthorizationService {
|
||||
}
|
||||
}
|
||||
|
||||
async checkAccess(action: PermissionsAction, collection: string, pk: PrimaryKey | PrimaryKey[]) {
|
||||
async checkAccess(
|
||||
action: PermissionsAction,
|
||||
collection: string,
|
||||
pk: PrimaryKey | PrimaryKey[]
|
||||
) {
|
||||
const itemsService = new ItemsService(collection, { accountability: this.accountability });
|
||||
|
||||
try {
|
||||
@@ -247,8 +251,11 @@ export default class AuthorizationService {
|
||||
if (Array.isArray(pk) && result.length !== pk.length) throw '';
|
||||
} catch {
|
||||
throw new ForbiddenException(
|
||||
`You're not allowed to ${action} item "${pk}" in collection "${collection}".`, {
|
||||
collection, item: pk, action
|
||||
`You're not allowed to ${action} item "${pk}" in collection "${collection}".`,
|
||||
{
|
||||
collection,
|
||||
item: pk,
|
||||
action,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -205,7 +205,12 @@ export default class CollectionsService {
|
||||
const keys = Array.isArray(key) ? key : [key];
|
||||
|
||||
for (const key of keys) {
|
||||
const exists = await this.knex.select('collection').from('directus_collections').where({ collection: key }).first() !== undefined;
|
||||
const exists =
|
||||
(await this.knex
|
||||
.select('collection')
|
||||
.from('directus_collections')
|
||||
.where({ collection: key })
|
||||
.first()) !== undefined;
|
||||
|
||||
if (exists) {
|
||||
await collectionItemsService.update(payload.meta, key);
|
||||
@@ -238,7 +243,10 @@ export default class CollectionsService {
|
||||
throw new ForbiddenException('Only admins can perform this action.');
|
||||
}
|
||||
|
||||
const fieldsService = new FieldsService({ knex: this.knex, accountability: this.accountability });
|
||||
const fieldsService = new FieldsService({
|
||||
knex: this.knex,
|
||||
accountability: this.accountability,
|
||||
});
|
||||
|
||||
const tablesInDatabase = await schemaInspector.tables();
|
||||
|
||||
@@ -266,10 +274,14 @@ export default class CollectionsService {
|
||||
const isM2O = relation.many_collection === collection;
|
||||
|
||||
if (isM2O) {
|
||||
await this.knex('directus_relations').delete().where({ many_collection: collection, many_field: relation.many_field });
|
||||
await this.knex('directus_relations')
|
||||
.delete()
|
||||
.where({ many_collection: collection, many_field: relation.many_field });
|
||||
await fieldsService.deleteField(relation.one_collection, relation.one_field);
|
||||
} else {
|
||||
await this.knex('directus_relations').update({ one_field: null }).where({ one_collection: collection, field: relation.one_field });
|
||||
await this.knex('directus_relations')
|
||||
.update({ one_field: null })
|
||||
.where({ one_collection: collection, field: relation.one_field });
|
||||
await fieldsService.deleteField(relation.many_collection, relation.many_field);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export default class FieldsService {
|
||||
if (collection) {
|
||||
fields = (await nonAuthorizedItemsService.readByQuery({
|
||||
filter: { collection: { _eq: collection } },
|
||||
limit: -1
|
||||
limit: -1,
|
||||
})) as FieldMeta[];
|
||||
} else {
|
||||
fields = (await nonAuthorizedItemsService.readByQuery({ limit: -1 })) as FieldMeta[];
|
||||
@@ -102,11 +102,16 @@ export default class FieldsService {
|
||||
|
||||
// Filter the result so we only return the fields you have read access to
|
||||
if (this.accountability && this.accountability.admin !== true) {
|
||||
const permissions = await this.knex.select('collection', 'fields').from('directus_permissions').where({ role: this.accountability.role, action: 'read' });
|
||||
const permissions = await this.knex
|
||||
.select('collection', 'fields')
|
||||
.from('directus_permissions')
|
||||
.where({ role: this.accountability.role, action: 'read' });
|
||||
const allowedFieldsInCollection: Record<string, string[]> = {};
|
||||
|
||||
permissions.forEach((permission) => {
|
||||
allowedFieldsInCollection[permission.collection] = (permission.fields || '').split(',');
|
||||
allowedFieldsInCollection[permission.collection] = (permission.fields || '').split(
|
||||
','
|
||||
);
|
||||
});
|
||||
|
||||
if (collection && allowedFieldsInCollection.hasOwnProperty(collection) === false) {
|
||||
@@ -114,7 +119,8 @@ export default class FieldsService {
|
||||
}
|
||||
|
||||
return result.filter((field) => {
|
||||
if (allowedFieldsInCollection.hasOwnProperty(field.collection) === false) return false;
|
||||
if (allowedFieldsInCollection.hasOwnProperty(field.collection) === false)
|
||||
return false;
|
||||
const allowedFields = allowedFieldsInCollection[field.collection];
|
||||
if (allowedFields[0] === '*') return true;
|
||||
return allowedFields.includes(field.field);
|
||||
@@ -132,8 +138,9 @@ export default class FieldsService {
|
||||
.where({
|
||||
role: this.accountability.role,
|
||||
collection,
|
||||
action: 'read'
|
||||
}).first();
|
||||
action: 'read',
|
||||
})
|
||||
.first();
|
||||
|
||||
if (!permissions) throw new ForbiddenException();
|
||||
if (permissions.fields !== '*') {
|
||||
@@ -248,17 +255,20 @@ export default class FieldsService {
|
||||
.first();
|
||||
|
||||
if (record) {
|
||||
await this.itemsService.update({
|
||||
...field.meta,
|
||||
collection: collection,
|
||||
field: field.field,
|
||||
}, record.id);
|
||||
await this.itemsService.update(
|
||||
{
|
||||
...field.meta,
|
||||
collection: collection,
|
||||
field: field.field,
|
||||
},
|
||||
record.id
|
||||
);
|
||||
} else {
|
||||
await this.itemsService.create({
|
||||
...field.meta,
|
||||
collection: collection,
|
||||
field: field.field,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,10 +299,14 @@ export default class FieldsService {
|
||||
const isM2O = relation.many_collection === collection && relation.many_field === field;
|
||||
|
||||
if (isM2O) {
|
||||
await this.knex('directus_relations').delete().where({ many_collection: collection, many_field: field });
|
||||
await this.knex('directus_relations')
|
||||
.delete()
|
||||
.where({ many_collection: collection, many_field: field });
|
||||
await this.deleteField(relation.one_collection, relation.one_field);
|
||||
} else {
|
||||
await this.knex('directus_relations').update({ one_field: null }).where({ one_collection: collection, one_field: field });
|
||||
await this.knex('directus_relations')
|
||||
.update({ one_field: null })
|
||||
.where({ one_collection: collection, one_field: field });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +246,10 @@ export default class ItemsService implements AbstractService {
|
||||
columns.map(({ column }) => column)
|
||||
);
|
||||
|
||||
payloadWithoutAliases = await payloadService.processValues('update', payloadWithoutAliases);
|
||||
payloadWithoutAliases = await payloadService.processValues(
|
||||
'update',
|
||||
payloadWithoutAliases
|
||||
);
|
||||
|
||||
if (Object.keys(payloadWithoutAliases).length > 0) {
|
||||
await trx(this.collection)
|
||||
|
||||
@@ -17,7 +17,12 @@ import env from '../env';
|
||||
type Action = 'create' | 'read' | 'update';
|
||||
|
||||
type Transformers = {
|
||||
[type: string]: (action: Action, value: any, payload: Partial<Item>, accountability: Accountability | null) => Promise<any>;
|
||||
[type: string]: (
|
||||
action: Action,
|
||||
value: any,
|
||||
payload: Partial<Item>,
|
||||
accountability: Accountability | null
|
||||
) => Promise<any>;
|
||||
};
|
||||
|
||||
export default class PayloadService {
|
||||
@@ -117,7 +122,7 @@ export default class PayloadService {
|
||||
async 'date-updated'(action, value) {
|
||||
if (action === 'update') return new Date();
|
||||
return value;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
processValues(action: Action, payloads: Partial<Item>[]): Promise<Partial<Item>[]>;
|
||||
@@ -148,7 +153,12 @@ export default class PayloadService {
|
||||
processedPayload.map(async (record: any) => {
|
||||
await Promise.all(
|
||||
specialFieldsInCollection.map(async (field) => {
|
||||
const newValue = await this.processField(field, record, action, this.accountability);
|
||||
const newValue = await this.processField(
|
||||
field,
|
||||
record,
|
||||
action,
|
||||
this.accountability
|
||||
);
|
||||
if (newValue !== undefined) record[field.field] = newValue;
|
||||
})
|
||||
);
|
||||
@@ -180,8 +190,7 @@ export default class PayloadService {
|
||||
) {
|
||||
if (!field.special) return payload[field.field];
|
||||
|
||||
const fieldSpecials = field.special.split(',').map(s => s.trim());
|
||||
|
||||
const fieldSpecials = field.special.split(',').map((s) => s.trim());
|
||||
|
||||
let value = clone(payload[field.field]);
|
||||
|
||||
@@ -291,12 +300,19 @@ export default class PayloadService {
|
||||
);
|
||||
|
||||
const toBeUpdated = relatedRecords.filter(
|
||||
(record) => record.hasOwnProperty(relation.many_primary) === true && record.hasOwnProperty('$delete') === false
|
||||
(record) =>
|
||||
record.hasOwnProperty(relation.many_primary) === true &&
|
||||
record.hasOwnProperty('$delete') === false
|
||||
);
|
||||
|
||||
const toBeDeleted = relatedRecords
|
||||
.filter(record => record.hasOwnProperty(relation.many_primary) === true && record.hasOwnProperty('$delete') && record.$delete === true)
|
||||
.map(record => record[relation.many_primary]);
|
||||
.filter(
|
||||
(record) =>
|
||||
record.hasOwnProperty(relation.many_primary) === true &&
|
||||
record.hasOwnProperty('$delete') &&
|
||||
record.$delete === true
|
||||
)
|
||||
.map((record) => record[relation.many_primary]);
|
||||
|
||||
await itemsService.create(toBeCreated);
|
||||
await itemsService.update(toBeUpdated);
|
||||
|
||||
@@ -22,7 +22,10 @@ export default class ServerService {
|
||||
}
|
||||
|
||||
const osType = os.type() === 'Darwin' ? 'macOS' : os.type();
|
||||
const osVersion = osType === 'macOS' ? `${macosRelease().name} (${macosRelease().version})` : os.release();
|
||||
const osVersion =
|
||||
osType === 'macOS'
|
||||
? `${macosRelease().name} (${macosRelease().version})`
|
||||
: os.release();
|
||||
|
||||
return {
|
||||
directus: {
|
||||
@@ -37,7 +40,7 @@ export default class ServerService {
|
||||
version: osVersion,
|
||||
uptime: Math.round(os.uptime()),
|
||||
totalmem: os.totalmem(),
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,10 @@ export default class UsersService extends ItemsService {
|
||||
}
|
||||
|
||||
async acceptInvite(token: string, password: string) {
|
||||
const { email, scope } = jwt.verify(token, env.SECRET as string) as { email: string, scope: string };
|
||||
const { email, scope } = jwt.verify(token, env.SECRET as string) as {
|
||||
email: string;
|
||||
scope: string;
|
||||
};
|
||||
|
||||
if (scope !== 'invite') throw new ForbiddenException();
|
||||
|
||||
@@ -82,14 +85,17 @@ export default class UsersService extends ItemsService {
|
||||
if (!user) throw new ForbiddenException();
|
||||
|
||||
const payload = { email, scope: 'password-reset' };
|
||||
const token = jwt.sign(payload, env.SECRET as string, { expiresIn: '7d', });
|
||||
const token = jwt.sign(payload, env.SECRET as string, { expiresIn: '7d' });
|
||||
const acceptURL = env.PUBLIC_URL + '/admin/reset-password?token=' + token;
|
||||
|
||||
await sendPasswordResetMail(email, acceptURL);
|
||||
}
|
||||
|
||||
async resetPassword(token: string, password: string) {
|
||||
const { email, scope } = jwt.verify(token, env.SECRET as string) as { email: string, scope: string };
|
||||
const { email, scope } = jwt.verify(token, env.SECRET as string) as {
|
||||
email: string;
|
||||
scope: string;
|
||||
};
|
||||
|
||||
if (scope !== 'password-reset') throw new ForbiddenException();
|
||||
|
||||
@@ -111,7 +117,11 @@ export default class UsersService extends ItemsService {
|
||||
}
|
||||
|
||||
async enableTFA(pk: string) {
|
||||
const user = await this.knex.select('tfa_secret').from('directus_users').where({ id: pk }).first();
|
||||
const user = await this.knex
|
||||
.select('tfa_secret')
|
||||
.from('directus_users')
|
||||
.where({ id: pk })
|
||||
.first();
|
||||
|
||||
if (user?.tfa_secret !== null) {
|
||||
throw new InvalidPayloadException('TFA Secret is already set for this user');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AbstractServiceOptions, Accountability, PrimaryKey } from "../types";
|
||||
import { AbstractServiceOptions, Accountability, PrimaryKey } from '../types';
|
||||
import database from '../database';
|
||||
import Knex from 'knex';
|
||||
import { InvalidPayloadException, ForbiddenException } from '../exceptions';
|
||||
@@ -13,7 +13,7 @@ export default class UtilsService {
|
||||
this.accountability = options?.accountability || null;
|
||||
}
|
||||
|
||||
async sort(collection: string, { item, to }: { item: PrimaryKey, to: PrimaryKey }) {
|
||||
async sort(collection: string, { item, to }: { item: PrimaryKey; to: PrimaryKey }) {
|
||||
const schemaInspector = SchemaInspector(this.knex);
|
||||
|
||||
const sortFieldResponse = await this.knex
|
||||
@@ -25,7 +25,9 @@ export default class UtilsService {
|
||||
const sortField = sortFieldResponse?.sort_field;
|
||||
|
||||
if (!sortField) {
|
||||
throw new InvalidPayloadException(`Collection "${collection}" doesn't have a sort field.`);
|
||||
throw new InvalidPayloadException(
|
||||
`Collection "${collection}" doesn't have a sort field.`
|
||||
);
|
||||
}
|
||||
|
||||
if (this.accountability?.admin !== true) {
|
||||
@@ -60,10 +62,7 @@ export default class UtilsService {
|
||||
.first();
|
||||
|
||||
if (countResponse?.count && +countResponse.count !== 0) {
|
||||
const lastSortValueResponse = await this.knex
|
||||
.max(sortField)
|
||||
.from(collection)
|
||||
.first();
|
||||
const lastSortValueResponse = await this.knex.max(sortField).from(collection).first();
|
||||
|
||||
const rowsWithoutSortValue = await this.knex
|
||||
.select(primaryKeyField, sortField)
|
||||
@@ -80,14 +79,24 @@ export default class UtilsService {
|
||||
}
|
||||
}
|
||||
|
||||
const targetSortValueResponse = await this.knex.select(sortField).from(collection).where({ [primaryKeyField]: to }).first();
|
||||
const targetSortValueResponse = await this.knex
|
||||
.select(sortField)
|
||||
.from(collection)
|
||||
.where({ [primaryKeyField]: to })
|
||||
.first();
|
||||
const targetSortValue = targetSortValueResponse[sortField];
|
||||
|
||||
const sourceSortValueResponse = await this.knex.select(sortField).from(collection).where({ [primaryKeyField]: item }).first();
|
||||
const sourceSortValueResponse = await this.knex
|
||||
.select(sortField)
|
||||
.from(collection)
|
||||
.where({ [primaryKeyField]: item })
|
||||
.first();
|
||||
const sourceSortValue = sourceSortValueResponse[sortField];
|
||||
|
||||
// Set the target item to the new sort value
|
||||
await this.knex(collection).update({ [sortField]: targetSortValue }).where({ [primaryKeyField]: item });
|
||||
await this.knex(collection)
|
||||
.update({ [sortField]: targetSortValue })
|
||||
.where({ [primaryKeyField]: item });
|
||||
|
||||
if (sourceSortValue < targetSortValue) {
|
||||
await this.knex(collection)
|
||||
|
||||
@@ -4,13 +4,15 @@ import {
|
||||
StorageManagerConfig,
|
||||
Storage,
|
||||
} from '@slynova/flydrive';
|
||||
import camelcase from 'camelcase';
|
||||
import env from './env';
|
||||
import { validateEnv } from './utils/validate-env';
|
||||
import { getConfigFromEnv } from './utils/get-config-from-env';
|
||||
|
||||
/** @todo dynamically load these storage adapters */
|
||||
import { AmazonWebServicesS3Storage } from '@slynova/flydrive-s3';
|
||||
import { GoogleCloudStorage } from '@slynova/flydrive-gcs';
|
||||
|
||||
/** @todo dynamically load storage adapters here */
|
||||
validateEnv(['STORAGE_LOCATIONS']);
|
||||
|
||||
const storage = new StorageManager(getStorageConfig());
|
||||
|
||||
@@ -19,26 +21,25 @@ registerDrivers(storage);
|
||||
export default storage;
|
||||
|
||||
function getStorageConfig(): StorageManagerConfig {
|
||||
const config: any = { disks: {} };
|
||||
const config: StorageManagerConfig = {
|
||||
disks: {},
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
if (key.startsWith('STORAGE') === false) continue;
|
||||
if (key === 'STORAGE_LOCATIONS') continue;
|
||||
if (key.endsWith('PUBLIC_URL')) continue;
|
||||
const locations = env.STORAGE_LOCATIONS.split(',');
|
||||
|
||||
const disk = key.split('_')[1].toLowerCase();
|
||||
if (!config.disks[disk]) config.disks[disk] = { config: {} };
|
||||
locations.forEach((location: string) => {
|
||||
location = location.trim();
|
||||
|
||||
if (key.endsWith('DRIVER')) {
|
||||
config.disks[disk].driver = value;
|
||||
continue;
|
||||
}
|
||||
const diskConfig = {
|
||||
driver: env[`STORAGE_${location.toUpperCase()}_DRIVER`],
|
||||
config: getConfigFromEnv(`STORAGE_${location.toUpperCase()}_`),
|
||||
};
|
||||
|
||||
const configKey = camelcase(
|
||||
key.split('_').filter((_, index) => [0, 1].includes(index) === false)
|
||||
);
|
||||
config.disks[disk].config[configKey] = value;
|
||||
}
|
||||
delete diskConfig.config.publicUrl;
|
||||
delete diskConfig.config.driver;
|
||||
|
||||
config.disks![location] = diskConfig;
|
||||
});
|
||||
|
||||
return config;
|
||||
}
|
||||
@@ -53,6 +54,7 @@ function registerDrivers(storage: StorageManager) {
|
||||
|
||||
usedDrivers.forEach((driver) => {
|
||||
const storageDriver = getStorageDriver(driver);
|
||||
|
||||
if (storageDriver) {
|
||||
storage.registerDriver<Storage>(driver, storageDriver);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
export type PermissionsAction =
|
||||
| 'create'
|
||||
| 'read'
|
||||
| 'update'
|
||||
| 'delete'
|
||||
| 'comment'
|
||||
| 'explain';
|
||||
export type PermissionsAction = 'create' | 'read' | 'update' | 'delete' | 'comment' | 'explain';
|
||||
|
||||
export type Permission = {
|
||||
id: number;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { transform, isPlainObject } from 'lodash';
|
||||
|
||||
export function deepMap(obj: Record<string, any>, iterator: Function, context?: Function) {
|
||||
return transform(obj, function(result: any, val, key) {
|
||||
result[key] = isPlainObject(val) ? deepMap(val, iterator, context) : iterator.call(context, val, key, obj)
|
||||
return transform(obj, function (result: any, val, key) {
|
||||
result[key] = isPlainObject(val)
|
||||
? deepMap(val, iterator, context)
|
||||
: iterator.call(context, val, key, obj);
|
||||
});
|
||||
}
|
||||
|
||||
14
api/src/utils/get-config-from-env.ts
Normal file
14
api/src/utils/get-config-from-env.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import camelcase from 'camelcase';
|
||||
import env from '../env';
|
||||
|
||||
export function getConfigFromEnv(prefix: string, omitPrefix?: string) {
|
||||
const config: any = {};
|
||||
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
if (key.toLowerCase().startsWith(prefix.toLowerCase()) === false) continue;
|
||||
if (omitPrefix && key.toLowerCase().startsWith(omitPrefix.toLowerCase()) === true) continue;
|
||||
config[camelcase(key.slice(prefix.length))] = value;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Filter, Accountability } from "../types";
|
||||
import { Filter, Accountability } from '../types';
|
||||
import { deepMap } from './deep-map';
|
||||
|
||||
export function parseFilter(filter: Filter, accountability: Accountability | null) {
|
||||
|
||||
21
api/src/utils/validate-env.ts
Normal file
21
api/src/utils/validate-env.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import logger from '../logger';
|
||||
import env from '../env';
|
||||
|
||||
export function validateEnv(requiredKeys: string[]) {
|
||||
if (env.DB_CLIENT && env.DB_CLIENT === 'sqlite3') {
|
||||
requiredKeys.push('DB_FILENAME');
|
||||
} else {
|
||||
if (env.DB_CLIENT === 'pg') {
|
||||
requiredKeys.push('DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USER');
|
||||
} else {
|
||||
requiredKeys.push('DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USER', 'DB_PASSWORD');
|
||||
}
|
||||
}
|
||||
|
||||
for (const requiredKey of requiredKeys) {
|
||||
if (env.hasOwnProperty(requiredKey) === false) {
|
||||
logger.fatal(`Environment is missing the ${requiredKey} key.`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"target": "es6",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": false,
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"target": "es6",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": false,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"lib": [
|
||||
"es2019"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"lib": ["es2019"],
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{
|
||||
"defaultSeverity": "error",
|
||||
"extends": [
|
||||
"tslint:recommended"
|
||||
],
|
||||
"jsRules": {},
|
||||
"rules": {},
|
||||
"rulesDirectory": []
|
||||
}
|
||||
"defaultSeverity": "error",
|
||||
"extends": ["tslint:recommended"],
|
||||
"jsRules": {},
|
||||
"rules": {},
|
||||
"rulesDirectory": []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user