diff --git a/README.md b/README.md index 7bd997a..3aab4af 100644 --- a/README.md +++ b/README.md @@ -105,10 +105,6 @@ CERT_CHAIN=certificate.pem (required) CA_ROOT=fullchain.pem (optional) # ENV variables required for MODE: `server` -ACCESS_TOKEN_SECRET= -REFRESH_TOKEN_SECRET= -AUTH_CODE_SECRET= -SESSION_SECRET= DB_CONNECT=mongodb+srv://:@/?retryWrites=true&w=majority # options: [disable|enable] default: `disable` for `server` & `enable` for `desktop` diff --git a/api/.env.example b/api/.env.example index f38b982..dc8374c 100644 --- a/api/.env.example +++ b/api/.env.example @@ -12,10 +12,6 @@ PORT=[5000] default value is 5000 HELMET_CSP_CONFIG_PATH=./csp.config.json if omitted HELMET default will be used HELMET_COEP=[true|false] if omitted HELMET default will be used -ACCESS_TOKEN_SECRET= -REFRESH_TOKEN_SECRET= -AUTH_CODE_SECRET= -SESSION_SECRET= DB_CONNECT=mongodb+srv://:@/?retryWrites=true&w=majority RUN_TIMES=[sas|js|sas,js|js,sas] default considered as sas diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index d8d6f7d..4fe61f4 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -47,41 +47,6 @@ components: - userId type: object additionalProperties: false - LoginPayload: - properties: - username: - type: string - description: 'Username for user' - example: secretuser - password: - type: string - description: 'Password for user' - example: secretpassword - required: - - username - - password - type: object - additionalProperties: false - AuthorizeResponse: - properties: - code: - type: string - description: 'Authorization code' - example: someRandomCryptoString - required: - - code - type: object - additionalProperties: false - AuthorizePayload: - properties: - clientId: - type: string - description: 'Client ID' - example: clientID1 - required: - - clientId - type: object - additionalProperties: false ClientPayload: properties: clientId: @@ -440,13 +405,13 @@ components: type: object Pick__LeanDocument_T_.Exclude_keyof_LeanDocument_T_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested__: properties: - id: - description: 'The string version of this documents _id.' _id: $ref: '#/components/schemas/_LeanDocument__LeanDocument_T__' description: 'This documents _id.' __v: description: 'This documents __v.' + id: + description: 'The string version of this documents _id.' type: object description: 'From T, pick a set of properties whose keys are in the union K' Omit__LeanDocument_this_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested_: @@ -488,6 +453,41 @@ components: example: /Public/somefolder/some.file type: object additionalProperties: false + LoginPayload: + properties: + username: + type: string + description: 'Username for user' + example: secretuser + password: + type: string + description: 'Password for user' + example: secretpassword + required: + - username + - password + type: object + additionalProperties: false + AuthorizeResponse: + properties: + code: + type: string + description: 'Authorization code' + example: someRandomCryptoString + required: + - code + type: object + additionalProperties: false + AuthorizePayload: + properties: + clientId: + type: string + description: 'Client ID' + example: clientID1 + required: + - clientId + type: object + additionalProperties: false securitySchemes: bearerAuth: type: http @@ -558,86 +558,6 @@ paths: - bearerAuth: [] parameters: [] - /: - get: - operationId: Home - responses: - '200': - description: Ok - content: - application/json: - schema: - type: string - summary: 'Render index.html' - tags: - - Web - security: [] - parameters: [] - /SASLogon/login: - post: - operationId: Login - responses: - '200': - description: Ok - content: - application/json: - schema: - properties: - user: {properties: {displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [displayName, username, id], type: object} - loggedIn: {type: boolean} - required: - - user - - loggedIn - type: object - summary: 'Accept a valid username/password' - tags: - - Web - security: [] - parameters: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/LoginPayload' - /SASLogon/authorize: - post: - operationId: Authorize - responses: - '200': - description: Ok - content: - application/json: - schema: - $ref: '#/components/schemas/AuthorizeResponse' - examples: - 'Example 1': - value: {code: someRandomCryptoString} - summary: 'Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE' - tags: - - Web - security: [] - parameters: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/AuthorizePayload' - /SASLogon/logout: - get: - operationId: Logout - responses: - '200': - description: Ok - content: - application/json: - schema: {} - summary: 'Destroy the session stored in cookies' - tags: - - Web - security: [] - parameters: [] /SASjsApi/client: post: operationId: CreateClient @@ -1504,6 +1424,86 @@ paths: application/json: schema: $ref: '#/components/schemas/ExecuteReturnJsonPayload' + /: + get: + operationId: Home + responses: + '200': + description: Ok + content: + application/json: + schema: + type: string + summary: 'Render index.html' + tags: + - Web + security: [] + parameters: [] + /SASLogon/login: + post: + operationId: Login + responses: + '200': + description: Ok + content: + application/json: + schema: + properties: + user: {properties: {displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [displayName, username, id], type: object} + loggedIn: {type: boolean} + required: + - user + - loggedIn + type: object + summary: 'Accept a valid username/password' + tags: + - Web + security: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoginPayload' + /SASLogon/authorize: + post: + operationId: Authorize + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/AuthorizeResponse' + examples: + 'Example 1': + value: {code: someRandomCryptoString} + summary: 'Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE' + tags: + - Web + security: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AuthorizePayload' + /SASLogon/logout: + get: + operationId: Logout + responses: + '200': + description: Ok + content: + application/json: + schema: {} + summary: 'Destroy the session stored in cookies' + tags: + - Web + security: [] + parameters: [] servers: - url: / diff --git a/api/src/app.ts b/api/src/app.ts index e423b4f..de27567 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -1,5 +1,6 @@ import path from 'path' import express, { ErrorRequestHandler } from 'express' +import mongoose from 'mongoose' import csrf from 'csurf' import session from 'express-session' import MongoStore from 'connect-mongo' @@ -97,45 +98,44 @@ if (CORS === CorsType.ENABLED) { app.use(cors({ credentials: true, origin: whiteList })) } -/*********************************** - * DB Connection & * - * Express Sessions * - * With Mongo Store * - ***********************************/ -if (MODE === ModeType.Server) { - let store: MongoStore | undefined +export default setProcessVariables().then(async () => { + /*********************************** + * DB Connection & * + * Express Sessions * + * With Mongo Store * + ***********************************/ + if (MODE === ModeType.Server) { + let store: MongoStore | undefined - // NOTE: when exporting app.js as agent for supertest - // we should exclude connecting to the real database - if (process.env.NODE_ENV !== 'test') { - const clientPromise = connectDB().then((conn) => conn!.getClient() as any) + if (process.env.NODE_ENV !== 'test') { + store = MongoStore.create({ + client: mongoose.connection!.getClient() as any, + collectionName: 'sessions' + }) + } - store = MongoStore.create({ clientPromise, collectionName: 'sessions' }) + app.use( + session({ + secret: process.secrets.SESSION_SECRET, + saveUninitialized: false, // don't create session until something stored + resave: false, //don't save session if unmodified + store, + cookie: cookieOptions + }) + ) } - app.use( - session({ - secret: process.env.SESSION_SECRET as string, - saveUninitialized: false, // don't create session until something stored - resave: false, //don't save session if unmodified - store, - cookie: cookieOptions - }) - ) -} + app.use(express.json({ limit: '100mb' })) + app.use(express.static(path.join(__dirname, '../public'))) -app.use(express.json({ limit: '100mb' })) -app.use(express.static(path.join(__dirname, '../public'))) + const onError: ErrorRequestHandler = (err, req, res, next) => { + if (err.code === 'EBADCSRFTOKEN') + return res.status(400).send('Invalid CSRF token!') -const onError: ErrorRequestHandler = (err, req, res, next) => { - if (err.code === 'EBADCSRFTOKEN') - return res.status(400).send('Invalid CSRF token!') + console.error(err.stack) + res.status(500).send('Something broke!') + } - console.error(err.stack) - res.status(500).send('Something broke!') -} - -export default setProcessVariables().then(async () => { await setupFolders() await copySASjsCore() diff --git a/api/src/controllers/auth.ts b/api/src/controllers/auth.ts index e642213..778e3bf 100644 --- a/api/src/controllers/auth.ts +++ b/api/src/controllers/auth.ts @@ -129,8 +129,8 @@ const verifyAuthCode = async ( clientId: string, code: string ): Promise => { - return new Promise((resolve, reject) => { - jwt.verify(code, process.env.AUTH_CODE_SECRET as string, (err, data) => { + return new Promise((resolve) => { + jwt.verify(code, process.secrets.AUTH_CODE_SECRET, (err, data) => { if (err) return resolve(undefined) const clientInfo: InfoJWT = { diff --git a/api/src/controllers/group.ts b/api/src/controllers/group.ts index 3a6ff66..9a3bc40 100644 --- a/api/src/controllers/group.ts +++ b/api/src/controllers/group.ts @@ -20,7 +20,7 @@ export interface GroupResponse { description: string } -interface GroupDetailsResponse { +export interface GroupDetailsResponse { groupId: number name: string description: string @@ -249,9 +249,10 @@ const updateUsersListInGroup = async ( message: 'User not found.' } - const updatedGroup = (action === 'addUser' - ? await group.addUser(user._id) - : await group.removeUser(user._id)) as unknown as GroupDetailsResponse + const updatedGroup = + action === 'addUser' + ? await group.addUser(user) + : await group.removeUser(user) if (!updatedGroup) throw { @@ -260,9 +261,6 @@ const updateUsersListInGroup = async ( message: 'Unable to update group.' } - if (action === 'addUser') user.addGroup(group._id) - else user.removeGroup(group._id) - return { groupId: updatedGroup.groupId, name: updatedGroup.name, diff --git a/api/src/middlewares/authenticateToken.ts b/api/src/middlewares/authenticateToken.ts index 90c7027..f44c459 100644 --- a/api/src/middlewares/authenticateToken.ts +++ b/api/src/middlewares/authenticateToken.ts @@ -35,7 +35,7 @@ export const authenticateAccessToken: RequestHandler = async ( req, res, next, - process.env.ACCESS_TOKEN_SECRET as string, + process.secrets.ACCESS_TOKEN_SECRET, 'accessToken' ) } @@ -45,7 +45,7 @@ export const authenticateRefreshToken: RequestHandler = (req, res, next) => { req, res, next, - process.env.REFRESH_TOKEN_SECRET as string, + process.secrets.REFRESH_TOKEN_SECRET, 'refreshToken' ) } diff --git a/api/src/model/Configuration.ts b/api/src/model/Configuration.ts new file mode 100644 index 0000000..edcbbc1 --- /dev/null +++ b/api/src/model/Configuration.ts @@ -0,0 +1,45 @@ +import mongoose, { Schema } from 'mongoose' + +export interface ConfigurationType { + /** + * SecretOrPrivateKey to sign Access Token + * @example "someRandomCryptoString" + */ + ACCESS_TOKEN_SECRET: string + /** + * SecretOrPrivateKey to sign Refresh Token + * @example "someRandomCryptoString" + */ + REFRESH_TOKEN_SECRET: string + /** + * SecretOrPrivateKey to sign Auth Code + * @example "someRandomCryptoString" + */ + AUTH_CODE_SECRET: string + /** + * Secret used to sign the session cookie + * @example "someRandomCryptoString" + */ + SESSION_SECRET: string +} + +const ConfigurationSchema = new Schema({ + ACCESS_TOKEN_SECRET: { + type: String, + required: true + }, + REFRESH_TOKEN_SECRET: { + type: String, + required: true + }, + AUTH_CODE_SECRET: { + type: String, + required: true + }, + SESSION_SECRET: { + type: String, + required: true + } +}) + +export default mongoose.model('Configuration', ConfigurationSchema) diff --git a/api/src/model/Group.ts b/api/src/model/Group.ts index 36b7842..6341825 100644 --- a/api/src/model/Group.ts +++ b/api/src/model/Group.ts @@ -1,5 +1,6 @@ import mongoose, { Schema, model, Document, Model } from 'mongoose' -import User from './User' +import { GroupDetailsResponse } from '../controllers' +import User, { IUser } from './User' const AutoIncrement = require('mongoose-sequence')(mongoose) export interface GroupPayload { @@ -27,8 +28,9 @@ interface IGroupDocument extends GroupPayload, Document { } interface IGroup extends IGroupDocument { - addUser(userObjectId: Schema.Types.ObjectId): Promise - removeUser(userObjectId: Schema.Types.ObjectId): Promise + addUser(user: IUser): Promise + removeUser(user: IUser): Promise + hasUser(user: IUser): boolean } interface IGroupModel extends Model {} @@ -70,28 +72,31 @@ groupSchema.pre('remove', async function () { }) // Instance Methods -groupSchema.method( - 'addUser', - async function (userObjectId: Schema.Types.ObjectId) { - const userIdIndex = this.users.indexOf(userObjectId) - if (userIdIndex === -1) { - this.users.push(userObjectId) - } - this.markModified('users') - return this.save() +groupSchema.method('addUser', async function (user: IUser) { + const userObjectId = user._id + const userIdIndex = this.users.indexOf(userObjectId) + if (userIdIndex === -1) { + this.users.push(userObjectId) + user.addGroup(this._id) } -) -groupSchema.method( - 'removeUser', - async function (userObjectId: Schema.Types.ObjectId) { - const userIdIndex = this.users.indexOf(userObjectId) - if (userIdIndex > -1) { - this.users.splice(userIdIndex, 1) - } - this.markModified('users') - return this.save() + this.markModified('users') + return this.save() +}) +groupSchema.method('removeUser', async function (user: IUser) { + const userObjectId = user._id + const userIdIndex = this.users.indexOf(userObjectId) + if (userIdIndex > -1) { + this.users.splice(userIdIndex, 1) + user.removeGroup(this._id) } -) + this.markModified('users') + return this.save() +}) +groupSchema.method('hasUser', function (user: IUser) { + const userObjectId = user._id + const userIdIndex = this.users.indexOf(userObjectId) + return userIdIndex > -1 +}) export const Group: IGroupModel = model( 'Group', diff --git a/api/src/model/User.ts b/api/src/model/User.ts index b70807d..dd0123b 100644 --- a/api/src/model/User.ts +++ b/api/src/model/User.ts @@ -35,6 +35,7 @@ export interface UserPayload { } interface IUserDocument extends UserPayload, Document { + _id: Schema.Types.ObjectId id: number isAdmin: boolean isActive: boolean @@ -43,7 +44,7 @@ interface IUserDocument extends UserPayload, Document { tokens: [{ [key: string]: string }] } -interface IUser extends IUserDocument { +export interface IUser extends IUserDocument { comparePassword(password: string): boolean addGroup(groupObjectId: Schema.Types.ObjectId): Promise removeGroup(groupObjectId: Schema.Types.ObjectId): Promise diff --git a/api/src/types/system/process.d.ts b/api/src/types/system/process.d.ts index 1e56d75..d7eaf71 100644 --- a/api/src/types/system/process.d.ts +++ b/api/src/types/system/process.d.ts @@ -8,5 +8,6 @@ declare namespace NodeJS { appStreamConfig: import('../').AppStreamConfig logger: import('@sasjs/utils/logger').Logger runTimes: import('../../utils').RunTimeType[] + secrets: import('../../model/Configuration').ConfigurationType } } diff --git a/api/src/utils/connectDB.ts b/api/src/utils/connectDB.ts index b6dd383..9d47607 100644 --- a/api/src/utils/connectDB.ts +++ b/api/src/utils/connectDB.ts @@ -9,7 +9,5 @@ export const connectDB = async () => { } console.log('Connected to DB!') - await seedDB() - - return mongoose.connection + return seedDB() } diff --git a/api/src/utils/generateAccessToken.ts b/api/src/utils/generateAccessToken.ts index a924ab9..2b385b6 100644 --- a/api/src/utils/generateAccessToken.ts +++ b/api/src/utils/generateAccessToken.ts @@ -2,6 +2,6 @@ import jwt from 'jsonwebtoken' import { InfoJWT } from '../types' export const generateAccessToken = (data: InfoJWT) => - jwt.sign(data, process.env.ACCESS_TOKEN_SECRET as string, { + jwt.sign(data, process.secrets.ACCESS_TOKEN_SECRET, { expiresIn: '1day' }) diff --git a/api/src/utils/generateAuthCode.ts b/api/src/utils/generateAuthCode.ts index bfacb79..a5f95f3 100644 --- a/api/src/utils/generateAuthCode.ts +++ b/api/src/utils/generateAuthCode.ts @@ -2,6 +2,6 @@ import jwt from 'jsonwebtoken' import { InfoJWT } from '../types' export const generateAuthCode = (data: InfoJWT) => - jwt.sign(data, process.env.AUTH_CODE_SECRET as string, { + jwt.sign(data, process.secrets.AUTH_CODE_SECRET, { expiresIn: '30s' }) diff --git a/api/src/utils/generateRefreshToken.ts b/api/src/utils/generateRefreshToken.ts index b233572..a8365ff 100644 --- a/api/src/utils/generateRefreshToken.ts +++ b/api/src/utils/generateRefreshToken.ts @@ -2,6 +2,6 @@ import jwt from 'jsonwebtoken' import { InfoJWT } from '../types' export const generateRefreshToken = (data: InfoJWT) => - jwt.sign(data, process.env.REFRESH_TOKEN_SECRET as string, { + jwt.sign(data, process.secrets.REFRESH_TOKEN_SECRET, { expiresIn: '30 days' }) diff --git a/api/src/utils/getRunTimeAndFilePath.ts b/api/src/utils/getRunTimeAndFilePath.ts index b83cdee..259b0a9 100644 --- a/api/src/utils/getRunTimeAndFilePath.ts +++ b/api/src/utils/getRunTimeAndFilePath.ts @@ -5,7 +5,7 @@ import { RunTimeType } from '.' export const getRunTimeAndFilePath = async (programPath: string) => { const ext = path.extname(programPath) - // If programPath (_program) is provided with a ".sas" or ".js" extension + // If programPath (_program) is provided with a ".sas" or ".js" extension // we should use that extension to determine the appropriate runTime if (ext && Object.values(RunTimeType).includes(ext.slice(1) as RunTimeType)) { const runTime = ext.slice(1) diff --git a/api/src/utils/seedDB.ts b/api/src/utils/seedDB.ts index f63233f..7bff3a6 100644 --- a/api/src/utils/seedDB.ts +++ b/api/src/utils/seedDB.ts @@ -1,6 +1,73 @@ import Client from '../model/Client' +import Group from '../model/Group' import User from '../model/User' +import Configuration, { ConfigurationType } from '../model/Configuration' +import { randomBytes } from 'crypto' + +export const SECRETS: ConfigurationType = { + ACCESS_TOKEN_SECRET: randomBytes(64).toString('hex'), + REFRESH_TOKEN_SECRET: randomBytes(64).toString('hex'), + AUTH_CODE_SECRET: randomBytes(64).toString('hex'), + SESSION_SECRET: randomBytes(64).toString('hex') +} + +export const seedDB = async (): Promise => { + // Checking if client is already in the database + const clientExist = await Client.findOne({ clientId: CLIENT.clientId }) + if (!clientExist) { + const client = new Client(CLIENT) + await client.save() + + console.log(`DB Seed - client created: ${CLIENT.clientId}`) + } + + // Checking if 'AllUsers' Group is already in the database + let groupExist = await Group.findOne({ name: GROUP.name }) + if (!groupExist) { + const group = new Group(GROUP) + groupExist = await group.save() + + console.log(`DB Seed - Group created: ${GROUP.name}`) + } + + // Checking if user is already in the database + let usernameExist = await User.findOne({ username: ADMIN_USER.username }) + if (!usernameExist) { + const user = new User(ADMIN_USER) + usernameExist = await user.save() + + console.log(`DB Seed - admin account created: ${ADMIN_USER.username}`) + } + + if (!groupExist.hasUser(usernameExist)) { + groupExist.addUser(usernameExist) + console.log( + `DB Seed - admin account '${ADMIN_USER.username}' added to Group '${GROUP.name}'` + ) + } + + // checking if configuration is present in the database + let configExist = await Configuration.findOne() + if (!configExist) { + const configuration = new Configuration(SECRETS) + configExist = await configuration.save() + + console.log('DB Seed - configuration added') + } + + return { + ACCESS_TOKEN_SECRET: configExist.ACCESS_TOKEN_SECRET, + REFRESH_TOKEN_SECRET: configExist.REFRESH_TOKEN_SECRET, + AUTH_CODE_SECRET: configExist.AUTH_CODE_SECRET, + SESSION_SECRET: configExist.SESSION_SECRET + } +} + +const GROUP = { + name: 'AllUsers', + description: 'Group contains all users' +} const CLIENT = { clientId: 'clientID1', clientSecret: 'clientSecret' @@ -13,23 +80,3 @@ const ADMIN_USER = { isAdmin: true, isActive: true } - -export const seedDB = async () => { - // Checking if client is already in the database - const clientExist = await Client.findOne({ clientId: CLIENT.clientId }) - if (!clientExist) { - const client = new Client(CLIENT) - await client.save() - - console.log(`DB Seed - client created: ${CLIENT.clientId}`) - } - - // Checking if user is already in the database - const usernameExist = await User.findOne({ username: ADMIN_USER.username }) - if (!usernameExist) { - const user = new User(ADMIN_USER) - await user.save() - - console.log(`DB Seed - admin account created: ${ADMIN_USER.username}`) - } -} diff --git a/api/src/utils/setProcessVariables.ts b/api/src/utils/setProcessVariables.ts index f6f21b0..d1b3991 100644 --- a/api/src/utils/setProcessVariables.ts +++ b/api/src/utils/setProcessVariables.ts @@ -1,16 +1,28 @@ import path from 'path' import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils' -import { getDesktopFields, ModeType, RunTimeType } from '.' +import { connectDB, getDesktopFields, ModeType, RunTimeType, SECRETS } from '.' export const setProcessVariables = async () => { + const { MODE, RUN_TIMES } = process.env + + if (MODE === ModeType.Server) { + // NOTE: when exporting app.js as agent for supertest + // it should prevent connecting to the real database + if (process.env.NODE_ENV !== 'test') { + const secrets = await connectDB() + + process.secrets = secrets + } else { + process.secrets = SECRETS + } + } + if (process.env.NODE_ENV === 'test') { process.driveLoc = path.join(process.cwd(), 'sasjs_root') return } - const { MODE, RUN_TIMES } = process.env - process.runTimes = (RUN_TIMES?.split(',') as RunTimeType[]) ?? [] if (MODE === ModeType.Server) { diff --git a/api/src/utils/verifyEnvVariables.ts b/api/src/utils/verifyEnvVariables.ts index 8ab857f..84aa607 100644 --- a/api/src/utils/verifyEnvVariables.ts +++ b/api/src/utils/verifyEnvVariables.ts @@ -78,33 +78,7 @@ const verifyMODE = (): string[] => { } if (process.env.MODE === ModeType.Server) { - const { - ACCESS_TOKEN_SECRET, - REFRESH_TOKEN_SECRET, - AUTH_CODE_SECRET, - SESSION_SECRET, - DB_CONNECT - } = process.env - - if (!ACCESS_TOKEN_SECRET) - errors.push( - `- ACCESS_TOKEN_SECRET is required for PROTOCOL '${ModeType.Server}'` - ) - - if (!REFRESH_TOKEN_SECRET) - errors.push( - `- REFRESH_TOKEN_SECRET is required for PROTOCOL '${ModeType.Server}'` - ) - - if (!AUTH_CODE_SECRET) - errors.push( - `- AUTH_CODE_SECRET is required for PROTOCOL '${ModeType.Server}'` - ) - - if (!SESSION_SECRET) - errors.push( - `- SESSION_SECRET is required for PROTOCOL '${ModeType.Server}'` - ) + const { DB_CONNECT } = process.env if (process.env.NODE_ENV !== 'test') if (!DB_CONNECT)