Not able to configure for production environment

Hi, just recently I configured the development environment by using @forestadmin/agent. When I try to configure production environment, it doesn’t work and I am stuck with the message:

It looks like the URL is not responding

package.json

{
  "name": "cbl",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www",
    "start_dev": "nodemon ./bin/www",
    "docker": "docker-compose up --build"
  },
  "dependencies": {
    "@forestadmin/agent": "1.0.0-beta.43",
    "@forestadmin/datasource-sequelize": "1.0.0-beta.34",
    "bcryptjs": "^2.4.3",
    "cookie-parser": "~1.4.4",
    "cors": "^2.8.5",
    "date-and-time": "^2.4.1",
    "debug": "~2.6.9",
    "dotenv": "^16.0.1",
    "express": "~4.17.3",
    "express-validator": "^6.14.2",
    "handlebars": "^4.7.7",
    "handlebars-helpers": "^0.10.0",
    "http-errors": "~1.6.3",
    "jade": "^0.29.0",
    "jsonwebtoken": "^8.5.1",
    "mailgun.js": "^8.0.0",
    "moment": "^2.29.4",
    "morgan": "~1.9.1",
    "mysql2": "^2.3.3",
    "nodemailer": "^6.7.7",
    "passport": "^0.6.0",
    "passport-jwt": "^4.0.0",
    "rand-token": "^1.0.1",
    "sequelize": "^6.21.3",
    "sequelize-auto": "^0.8.8"
  },
  "devDependencies": {
    "nodemon": "^2.0.19",
    "sequelize-cli": "^6.4.1"
  }
}

app.js

const createError = require('http-errors');
const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const { createAgent } = require('@forestadmin/agent');
const {
  createSequelizeDataSource,
} = require('@forestadmin/datasource-sequelize');
require('dotenv').config();
const cors = require('cors');
const sequelizeInstance = require('./src/db/models/index').sequelize;

var indexRouter = require('./src/routes/index');
var authRouter = require('./src/routes/api/auth');
var usersRouter = require('./src/routes/api/users');
var ceremonyRouter = require('./src/routes/api/ceremony');
var questionRouter = require('./src/routes/api/questions');

var app = express();

console.log(process.env.FOREST_AUTH_SECRET);
console.log(process.env.FOREST_AGENT_URL);
console.log(process.env.FOREST_ENV_SECRET);
console.log(process.env.NODE_ENV);

// Create your Forest Admin agent
// This must be called BEFORE all other middlewares on the express app
createAgent({
  authSecret: process.env.FOREST_AUTH_SECRET,
  agentUrl: process.env.FOREST_AGENT_URL,
  envSecret: process.env.FOREST_ENV_SECRET,
  isProduction: process.env.NODE_ENV === 'production',
})
  .addDataSource(createSequelizeDataSource(sequelizeInstance))
  .mountOnExpress(app)
  .start();

let allowedOrigins = [/\.forestadmin\.com$/, /localhost:\d{4}$/];

if (process.env.ALLOWED_CORS) {
  allowedOrigins = allowedOrigins.concat(process.env.ALLOWED_CORS.split(','));
}

var corsOptions = {
  origin: allowedOrigins,
};

app.use(cors(corsOptions));

// view engine setup
app.set('views', path.join(__dirname, 'src/views'));
app.set('view engine', 'jade');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

//Routes
app.use('/', indexRouter);
app.use('/api', indexRouter);
app.use('/api/auth', authRouter);
app.use('/api/users', usersRouter);
app.use('/api/ceremony', ceremonyRouter);
app.use('/api/questions', questionRouter);

// catch 404 and forward to error handler
app.use(function (req, res, next) {
  next(createError(404));
});

// error handler
app.use(function (err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};
  res.status(err.status || 500);
  res.render('error');
});

// set port, listen for requests
const PORT = process.env.NODE_DOCKER_PORT || 8080;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}.`);
});

module.exports = app;

/src/db/models/index.js

'use strict';

const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../../config/database.js')[env];
const db = {};

let sequelize;
if (config.use_env_variable) {
  sequelize = new Sequelize(process.env[config.use_env_variable], {
    ...config,
    host: process.env.USING_DOCKER ? 'host.docker.internal' : config.host,
  });
} else {
  sequelize = new Sequelize(config.database, config.username, config.password, {
    ...config,
    host: process.env.USING_DOCKER ? 'host.docker.internal' : config.host,
  });
}

sequelize
  .authenticate()
  .then(() => {
    console.log('Connection has been established successfully.');
  })
  .catch((err) => {
    console.error('Unable to connect to the database:', err);
  });

fs.readdirSync(__dirname)
  .filter((file) => {
    return (
      file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js'
    );
  })
  .forEach((file) => {
    const model = require(path.join(__dirname, file))(
      sequelize,
      Sequelize.DataTypes
    );
    db[model.name] = model;
  });

Object.keys(db).forEach((modelName) => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

db.sequelize = sequelize;

module.exports = db;

src/config.database.js

require('dotenv').config();

module.exports = {
  development: {
    username: process.env.MYSQLDB_USER,
    password: process.env.MYSQLDB_ROOT_PASSWORD,
    database: process.env.MYSQLDB_DATABASE,
    host: '127.0.0.1',
    port: process.env.MYSQLDB_LOCAL_PORT,
    dialect: 'mysql',
  },
  test: {
    username: process.env.MYSQLDB_USER_STAGE,
    password: process.env.MYSQLDB_ROOT_PASSWORD_STAGE,
    database: process.env.MYSQLDB_DATABASE_STAGE,
    host: '127.0.0.1',
    dialect: 'mysql',
  },
  production: {
    username: process.env.MYSQLDB_USER_PROD,
    password: process.env.MYSQLDB_ROOT_PASSWORD_PROD,
    database: process.env.MYSQLDB_DATABASE_PROD,
    host: '127.0.0.1',
    dialect: 'mysql',
  },
};

bin/www

#!/usr/bin/env node

/**
 * Module dependencies.
 */

var app = require('../app');
var debug = require('debug')('cbl-backend:server');
var http = require('http');

/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

/**
 * Create HTTP server.
 */

var server = http.createServer(app);

/**
 * Listen on provided port, on all network interfaces.
 */

server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

/**
 * Normalize a port into a number, string, or false.
 */

function normalizePort(val) {
  var port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

/**
 * Event listener for HTTP server "error" event.
 */

function onError(error) {
  if (error.syscall !== 'listen') {
    throw error;
  }

  var bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges');
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(bind + ' is already in use');
      process.exit(1);
      break;
    default:
      throw error;
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */

function onListening() {
  var addr = server.address();
  var bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  debug('Listening on ' + bind);
}

The app is deployed on aws ec2 ubuntu without docker. I use docker only on localhost. MySQL is used as a database and I use only default configuration, there is no require_secure_transport. I turned off the firewall on ubuntu for now.

3306 port is opened in inbound connections:

I can connect to production database from my local machine:

Pm2 is used as a process manager, here are the logs where you can see that process.env.FOREST_AUTH_SECRET, process.env.FOREST_AGENT_URL, process.env.FOREST_ENV_SECRET, process.env.NODE_ENV exist:

There are no errors in pm2 error log file:

.forestadmin-schema.json file is on the server:

this is the DATABASE_URL:

What’s wrong? How can I debug this issue?

Hello, are you missing the “m” from the .com in your forest_agent_url ?

No, I don’t. The domain is .co

Are you able to ping your app ? Do you register this domain on your route 53 ? You instance is linked to this domain ?

Yes, I am able to ping my app. Domain is registered and working with my instance.

And api/forest does not exist ?

Yes, exactly. My api routes work, but forest routes throw 404.

Can you try to run the agent after listening please ?

const agent = createAgent({
  authSecret: process.env.FOREST_AUTH_SECRET,
  agentUrl: process.env.FOREST_AGENT_URL,
  envSecret: process.env.FOREST_ENV_SECRET,
  isProduction: process.env.NODE_ENV === 'production',
})
  .addDataSource(createSequelizeDataSource(sequelizeInstance))
  .mountOnExpress(app);

...

app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}.`);
});

...

await agent.start();

If I try this, I get an error:

TypeError: agent.start is not a function
    at /var/apps/cbl_prod/cbl-backend/app.js:92:18
    at Object.<anonymous> (/var/apps/cbl_prod/cbl-backend/app.js:96:3)
    at Module._compile (node:internal/modules/cjs/loader:1105:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1159:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Module.require (node:internal/modules/cjs/loader:1005:19)
    at require (node:internal/modules/cjs/helpers:102:18)
    at Object.<anonymous> (/var/apps/cbl_prod/cbl-backend/bin/www:7:11)
    at Module._compile (node:internal/modules/cjs/loader:1105:14)

If I console.log agent variables it’s a pending promise.

It is strange. You have a working example here.

I had an issue in my code. I was perhpas missing await when I was creating new agent.

This is my current app.js:

const createError = require('http-errors');
const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const { createAgent } = require('@forestadmin/agent');
const {
  createSequelizeDataSource,
} = require('@forestadmin/datasource-sequelize');
require('dotenv').config();
const cors = require('cors');
const sequelizeInstance = require('./src/db/models/index').sequelize;

var indexRouter = require('./src/routes/index');
var authRouter = require('./src/routes/api/auth');
var usersRouter = require('./src/routes/api/users');
var ceremonyRouter = require('./src/routes/api/ceremony');
var questionRouter = require('./src/routes/api/questions');

var app = express();

console.log(process.env.FOREST_AUTH_SECRET);
console.log(process.env.FOREST_AGENT_URL);
console.log(process.env.FOREST_ENV_SECRET);
console.log(process.env.NODE_ENV);

let allowedOrigins = [/\.forestadmin\.com$/, /localhost:\d{4}$/];

if (process.env.ALLOWED_CORS) {
  allowedOrigins = allowedOrigins.concat(process.env.ALLOWED_CORS.split(','));
}

var corsOptions = {
  origin: allowedOrigins,
  allowedHeaders: ['Authorization', 'X-Requested-With', 'Content-Type'],
  maxAge: 86400, // NOTICE: 1 day
  credentials: true,
};

app.use('/forest/authentication', cors({
  ...corsOptions,
  // The null origin is sent by browsers for redirected AJAX calls
  // we need to support this in authentication routes because OIDC
  // redirects to the callback route
  origin: corsOptions.origin.concat('null')
}));

app.use(cors(corsOptions));

// view engine setup
app.set('views', path.join(__dirname, 'src/views'));
app.set('view engine', 'jade');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

//Routes
app.use('/', indexRouter);
app.use('/api', indexRouter);
app.use('/api/auth', authRouter);
app.use('/api/users', usersRouter);
app.use('/api/ceremony', ceremonyRouter);
app.use('/api/questions', questionRouter);

// catch 404 and forward to error handler
app.use(function (req, res, next) {
  next(createError(404));
});

// error handler
app.use(function (err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};
  res.status(err.status || 500);
  res.render('error');
});

// set port, listen for requests
const PORT = process.env.NODE_DOCKER_PORT || 8080;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}.`);
});

(async () => {
  try {
  agent = await createAgent({
  authSecret: process.env.FOREST_AUTH_SECRET,
  agentUrl: process.env.FOREST_AGENT_URL,
  envSecret: process.env.FOREST_ENV_SECRET,
  isProduction: process.env.NODE_ENV === 'production',
})
  .addDataSource(createSequelizeDataSource(sequelizeInstance))
  .mountOnExpress(app)
  .start();
} catch (e) {
    // Deal with the fact the chain failed
  }
})();

I am still getting the 404 error for https://app.prxxxxxxxxxx.co/api/forest.

Are you sure is working on dev environment ?

I think I found what’s the issue, but I am not really sure how to fix this.

My nginx configuration is set up like this:

server {

        root /var/apps/cbl_prod/cbl-frontend/build;

        # Add index.php to the list if you are using PHP
        index index.html index.htm index.nginx-debian.html;

        server_name app.pxxxxxxx.co www.app.prxxxxxxx.co;

        location / {
                # First attempt to serve request as file, then
                # as directory, then fall back to displaying a 404.
                try_files $uri /index.html;
        }

        location /api {
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $http_host;
            proxy_set_header X-NginX-Proxy true;

            proxy_pass http://127.0.0.1:8081;
            proxy_redirect off;
        }

So when you call app.pxxxxxxx.co, you get the html file. When you call app.pxxxxxxx.co/api, you make a call to the node.js server.

I just tried to make a call: curl localhost:8081/forest/ and I get the response {"error":null,"message":"Agent is running"}

So actually the agent is running. The problem is that when forestadmin app makes a call to https://app.prxxxxxxxx.co/api/forest, it hits the server with localhost:8081/api/forest/, but this route doesn’t exist, because the agent operates without /api route in the middle. Do you have an idea how I can fix this?

You have two solutions:

  1. During the setup on the forest interface, remove the api.
  2. Add prefix: '/api/forest' attribute, in the plain object of the createAgent method.

I tried the prefix option, but it doesn’t work for me.

I updated my nginx configuration and now it’s working.

location /api/forest {
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $http_host;
            proxy_set_header X-NginX-Proxy true;

            proxy_pass http://127.0.0.1:8081/forest;
            proxy_redirect off;
        }

        location /api {
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $http_host;
            proxy_set_header X-NginX-Proxy true;

            proxy_pass http://127.0.0.1:8081;
            proxy_redirect off;
        }

Nice !
I hope all will be ok for you now :slight_smile:

1 Like

I am not sure if it’s in the documentation, but it would be great if you can add it

  1. It’s possible to check if forest admin is running by pinging serveraddress/forest and the response is that agent is running.

  2. createAgent function parameters.

Thanks for the feedback. I will create some issues to improve it. :pray: