Skip to content

Commit

Permalink
refactor(condo): DOMA-5909 add prepareKeystone (#3197)
Browse files Browse the repository at this point in the history
  • Loading branch information
pahaz authored May 5, 2023
1 parent e8c6ec8 commit e2f634b
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 208 deletions.
3 changes: 1 addition & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ PORT=3000
SERVER_URL=http://localhost:3000
DEFAULT_LOCALE=ru
FILE_FIELD_ADAPTER=local
FAKE_ADDRESS_SUGGESTIONS=true
GOOGLE_RECAPTCHA_CONFIG='{"SITE_KEY":"6LcPRvQaAAAAAJRyxsFIB4rP5VH036pFOkNH8lgh", "SERVER_KEY":"6LcPRvQaAAAAADn_h1440Es7fXIGD0E4lpXR_FyF"}'
HELP_REQUISITES='{ "support_email": "help@doma.ai", "support_email_mobile": "helpmobile@doma.ai", "bot_email": "service@doma.ai", "support_phone": "8 800 700-36-62" }'
HELP_REQUISITES='{ "support_email": "help@example.com", "support_email_mobile": "helpmobile@example.com", "bot_email": "service@example.com", "support_phone": "+1 301 000-00-00" }'

# Cache settings
ADAPTER_CACHE_CONFIG = '{ "enabled": true, "excludedLists":[], "logging":0, "maxCacheSize":1000, "logStatsEachSecs": 60 }'
Expand Down
20 changes: 20 additions & 0 deletions apps/condo/domains/common/utils/VersioningMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const packageJson = require('@app/condo/package.json')
const express = require('express')
const { get } = require('lodash')

class VersioningMiddleware {
async prepareMiddleware () {
const app = express()
app.use('/api/version', (req, res) => {
res.status(200).json({
build: get(process.env, 'WERF_COMMIT_HASH', packageJson.version),
})
})

return app
}
}

module.exports = {
VersioningMiddleware,
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const express = require('express')

const { expressErrorHandler } = require('@condo/domains/common/utils/expressErrorHandler')
const { expressErrorHandler } = require('@open-condo/keystone/logging/expressErrorHandler')

const { SbbolRoutes } = require('@condo/domains/organization/integrations/sbbol/routes')
const { SberIdRoutes } = require('@condo/domains/user/integration/sberid/routes')

Expand Down
240 changes: 56 additions & 184 deletions apps/condo/index.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,21 @@
const packageJson = require('@app/condo/package.json')
const { AdminUIApp } = require('@keystonejs/app-admin-ui')
const { GraphQLApp } = require('@keystonejs/app-graphql')
const { NextApp } = require('@keystonejs/app-next')
const { PasswordAuthStrategy } = require('@keystonejs/auth-password')
const { Keystone } = require('@keystonejs/keystone')
const { createItems } = require('@keystonejs/server-side-graphql-client')
const bodyParser = require('body-parser')
const dayjs = require('dayjs')
const duration = require('dayjs/plugin/duration')
const isBetween = require('dayjs/plugin/isBetween')
const timezone = require('dayjs/plugin/timezone')
const utc = require('dayjs/plugin/utc')
const express = require('express')
const { identity } = require('lodash')
const get = require('lodash/get')
const nextCookie = require('next-cookies')
const { v4 } = require('uuid')

const conf = require('@open-condo/config')
const { FeaturesMiddleware } = require('@open-condo/featureflags/FeaturesMiddleware')
const { AdapterCache } = require('@open-condo/keystone/adapterCache')
const { formatError } = require('@open-condo/keystone/apolloErrorFormatter')
const { registerSchemas } = require('@open-condo/keystone/KSv5v6/v5/registerSchema')
const { GraphQLLoggerPlugin, getKeystonePinoOptions } = require('@open-condo/keystone/logging')
const {
customAccessPostProcessor,
schemaDocPreprocessor,
adminDocPreprocessor,
escapeSearchPreprocessor,
} = require('@open-condo/keystone/preprocessors')
const { prepareKeystone } = require('@open-condo/keystone/KSv5v6/v5/prepareKeystone')
const { RequestCache } = require('@open-condo/keystone/requestCache')
const { prepareDefaultKeystoneConfig, getAdapter } = require('@open-condo/keystone/setup.utils')
const { getWebhookModels } = require('@open-condo/webhooks/schema')

const { PaymentLinkMiddleware } = require('@condo/domains/acquiring/PaymentLinkMiddleware')
const { parseCorsSettings } = require('@condo/domains/common/utils/cors.utils')
const { expressErrorHandler } = require('@condo/domains/common/utils/expressErrorHandler')
const FileAdapter = require('@condo/domains/common/utils/fileAdapter')
const { makeId } = require('@condo/domains/common/utils/makeid.utils')
const { hasValidJsonStructure } = require('@condo/domains/common/utils/validation.utils')
const { VersioningMiddleware } = require('@condo/domains/common/utils/VersioningMiddleware')
const { UserExternalIdentityMiddleware } = require('@condo/domains/user/integration/UserExternalIdentityMiddleware')
const { OIDCMiddleware } = require('@condo/domains/user/oidc')

Expand All @@ -47,62 +24,41 @@ dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(isBetween)

const FINGERPRINT_FORMAT_REGEXP = /^[a-zA-Z0-9!#$%()*+-;=,:[\]/.?@^_`{|}~]{5,42}$/

const IS_ENABLE_DD_TRACE = conf.NODE_ENV === 'production' && conf.DD_TRACE_ENABLED === 'true'
const IS_ENABLE_APOLLO_DEBUG = conf.NODE_ENV === 'development' || conf.NODE_ENV === 'test'

const IS_BUILD_PHASE = conf.PHASE === 'build'
const IS_ON_WORKER = conf.PHASE === 'worker'
// NOTE(pahaz): it's a magic number tested by @arichiv at https://developer.chrome.com/blog/cookie-max-age-expires/
const INFINITY_MAX_AGE_COOKIE = 1707195600

// TODO(zuch): DOMA-2990: add FILE_FIELD_ADAPTER to env during build phase
if (IS_BUILD_PHASE) {
process.env.FILE_FIELD_ADAPTER = 'local' // Test
}

// NOTE: should be disabled in production: https://www.apollographql.com/docs/apollo-server/testing/graphql-playground/
// WARN: https://github.com/graphql/graphql-playground/tree/main/packages/graphql-playground-html/examples/xss-attack
const IS_ENABLE_DANGEROUS_GRAPHQL_PLAYGROUND = conf.ENABLE_DANGEROUS_GRAPHQL_PLAYGROUND === 'true'

if (IS_ENABLE_DD_TRACE && !IS_BUILD_PHASE) {
require('dd-trace').init({
logInjection: true,
})
}

const keystoneConfig = (IS_BUILD_PHASE) ? {
cookieSecret: v4(),
adapter: getAdapter('undefined'),
} : prepareDefaultKeystoneConfig(conf)
const keystone = new Keystone({
...keystoneConfig,
onConnect: async () => {
// Initialise some data
if (conf.NODE_ENV !== 'development' && conf.NODE_ENV !== 'test') return // Just for dev env purposes!
// This function can be called before tables are created! (we just ignore this)
const users = await keystone.lists.User.adapter.findAll()
if (!users.length) {
const initialData = require('./initialData')
for (let { listKey, items } of initialData) {
console.log(`🗿 createItems(${listKey}) -> ${items.length}`)
await createItems({
keystone,
listKey,
items,
})
}
/** @deprecated */
const onConnect = async (keystone) => {
// Initialise some data
if (conf.NODE_ENV !== 'development' && conf.NODE_ENV !== 'test') return // Just for dev env purposes!
// This function can be called before tables are created! (we just ignore this)
const users = await keystone.lists.User.adapter.findAll()
if (!users.length) {
const initialData = require('./initialData')
for (let { listKey, items } of initialData) {
console.log(`🗿 createItems(${listKey}) -> ${items.length}`)
await createItems({
keystone,
listKey,
items,
})
}
},
})

// Because Babel is used only for frontend to transpile and optimise code,
// backend files will bring unnecessary workload to building stage.
// They can be safely ignored without impact on final executable code
}
}

// We need to register all schemas as they will appear in admin ui
registerSchemas(keystone, [
const schemas = () => [
require('@condo/domains/user/schema'),
require('@condo/domains/organization/schema'),
require('@condo/domains/property/schema'),
Expand All @@ -121,126 +77,42 @@ registerSchemas(keystone, [
require('@condo/domains/scope/schema'),
require('@condo/domains/news/schema'),
getWebhookModels('@app/condo/schema.graphql'),
], [schemaDocPreprocessor, adminDocPreprocessor, escapeSearchPreprocessor, customAccessPostProcessor])

if (!IS_BUILD_PHASE) {
// NOTE(pahaz): we put it here because it inits the redis connection and we don't want it at build time
const { registerTasks } = require('@open-condo/keystone/tasks')

registerTasks([
require('@condo/domains/acquiring/tasks'),
require('@condo/domains/notification/tasks'),
require('@condo/domains/organization/tasks'),
require('@condo/domains/ticket/tasks'),
require('@condo/domains/resident/tasks'),
require('@condo/domains/scope/tasks'),
require('@open-condo/webhooks/tasks'),
require('@condo/domains/news/tasks'),
])
]

const tasks = () => [
require('@condo/domains/acquiring/tasks'),
require('@condo/domains/notification/tasks'),
require('@condo/domains/organization/tasks'),
require('@condo/domains/ticket/tasks'),
require('@condo/domains/resident/tasks'),
require('@condo/domains/scope/tasks'),
require('@condo/domains/news/tasks'),
require('@open-condo/webhooks/tasks'),
]

const lastApp = conf.NODE_ENV === 'test' ? undefined : new NextApp({ dir: '.' })
const apps = () => [
new RequestCache(conf.REQUEST_CACHE_CONFIG ? JSON.parse(conf.REQUEST_CACHE_CONFIG) : {}),
new AdapterCache(conf.ADAPTER_CACHE_CONFIG ? JSON.parse(conf.ADAPTER_CACHE_CONFIG) : {}),
new VersioningMiddleware(),
new OIDCMiddleware(),
new FeaturesMiddleware(),
new PaymentLinkMiddleware(),
FileAdapter.makeFileAdapterMiddleware(),
new UserExternalIdentityMiddleware(),
]

/** @type {(app: import('express').Application) => void} */
const extendExpressApp = (app) => {
app.get('/.well-known/change-password', function (req, res) {
res.redirect('/auth/forgot')
})
}

const authStrategy = keystone.createAuthStrategy({
type: PasswordAuthStrategy,
list: 'User',
config: {
protectIdentities: false,
},
module.exports = prepareKeystone({
onConnect,
extendExpressApp,
schemas, tasks,
apps, lastApp,
ui: { hooks: require.resolve('@app/condo/admin-ui') },
})

class VersioningMiddleware {
async prepareMiddleware () {
const app = express()
app.use('/api/version', (req, res) => {
res.status(200).json({
build: get(process.env, 'WERF_COMMIT_HASH', packageJson.version),
})
})

return app
}
}

module.exports = {
keystone,
// NOTE(pahaz): please, check the `executeDefaultServer(..)` to understand how it works.
// And you need to look at `keystone/lib/Keystone/index.js:602` it uses `{ origin: true, credentials: true }` as default value for cors!
// Examples: https://expressjs.com/en/resources/middleware/cors.html or check `node_modules/cors/README.md`
cors: (conf.CORS) ? parseCorsSettings(JSON.parse(conf.CORS)) : { origin: true, credentials: true },
pinoOptions: getKeystonePinoOptions(),
apps: [
new RequestCache(conf.REQUEST_CACHE_CONFIG ? JSON.parse(conf.REQUEST_CACHE_CONFIG) : {}),
new AdapterCache(conf.ADAPTER_CACHE_CONFIG ? JSON.parse(conf.ADAPTER_CACHE_CONFIG) : {}),
new VersioningMiddleware(),
new OIDCMiddleware(),
new FeaturesMiddleware(),
new PaymentLinkMiddleware(),
new GraphQLApp({
apollo: {
formatError,
debug: IS_ENABLE_APOLLO_DEBUG,
introspection: IS_ENABLE_DANGEROUS_GRAPHQL_PLAYGROUND,
playground: IS_ENABLE_DANGEROUS_GRAPHQL_PLAYGROUND,
plugins: [new GraphQLLoggerPlugin()],
},
}),
FileAdapter.makeFileAdapterMiddleware(),
new UserExternalIdentityMiddleware(),
new AdminUIApp({
adminPath: '/admin',
isAccessAllowed: ({ authentication: { item: user } }) => Boolean(user && (user.isAdmin || user.isSupport)),
authStrategy,
hooks: require.resolve('@app/condo/admin-ui'),
}),
conf.NODE_ENV === 'test' || IS_ON_WORKER ? undefined : new NextApp({ dir: '.' }),
].filter(identity),

/** @type {(app: import('express').Application) => void} */
configureExpress: (app) => {
app.set('trust proxy', true)
// NOTE(toplenboren): we need a custom body parser for custom file upload limit
app.use(bodyParser.json({ limit: '100mb', extended: true }))
app.use(bodyParser.urlencoded({ limit: '100mb', extended: true }))

const requestIdHeaderName = 'X-Request-Id'
app.use(function reqId (req, res, next) {
const reqId = req.headers[requestIdHeaderName.toLowerCase()] || v4()
req['id'] = req.headers[requestIdHeaderName.toLowerCase()] = reqId
res.setHeader(requestIdHeaderName, reqId)
next()
})

app.get('/.well-known/change-password', function (req, res) {
res.redirect('/auth/forgot')
})

app.use('/admin/', (req, res, next) => {
if (req.url === '/api') return next()
const cookies = nextCookie({ req })
const isSenderValid = hasValidJsonStructure(
{
resolvedData: { sender: cookies['sender'] },
fieldPath: 'sender',
addFieldValidationError: () => null,
},
true,
1,
{
fingerprint: {
presence: true,
format: FINGERPRINT_FORMAT_REGEXP,
length: { minimum: 5, maximum: 42 },
},
})
if (!isSenderValid) {
const fingerprint = cookies['userId'] || makeId(12)
res.cookie('sender', JSON.stringify({ fingerprint, dv: 1 }), { maxAge: INFINITY_MAX_AGE_COOKIE })
res.cookie('dv', 1, { maxAge: INFINITY_MAX_AGE_COOKIE })
res.cookie('userId', fingerprint, { maxAge: INFINITY_MAX_AGE_COOKIE })
}
next()
})

// The next middleware must be the last one
app.use(expressErrorHandler)
},
}
Loading

0 comments on commit e2f634b

Please sign in to comment.