Search code examples
node.jsexpressforestadmin

Forest Admin returns 404 for /forest/authentication route


I am not able to set up Forest Admin with my local environment (Express and MySQL). When I try to authenticate, I am getting 404 for /forest/authentication route.

enter image description here

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 jwt = require('express-jwt');
const Sequelize = require('sequelize');
const forest = require('forest-express-sequelize');
const { errorHandler } = require('forest-express-sequelize');
// 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();

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

var corsOptions = {
  origin: allowedOrigins,
  maxAge: 86400, // NOTICE: 1 day
  credentials: true,
};

// Support for request-private-network as the `cors` package
// doesn't support it by default
// See: https://github.com/expressjs/cors/issues/236
app.use((req, res, next) => {
  if (req.headers['access-control-request-private-network']) {
    res.setHeader('access-control-allow-private-network', 'true');
  }
  next(null);
});
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}.`);
});

// // 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();

app.use(
  jwt({
    secret: process.env.FOREST_AUTH_SECRET,
    credentialsRequired: false,
    algorithms: ['HS256'],
  })
);

forest
  .init({
    envSecret: process.env.FOREST_ENV_SECRET,
    authSecret: process.env.FOREST_AUTH_SECRET,
    objectMapping: Sequelize,
    connections: { default: sequelizeInstance },
  })
  .then((FAMiddleware) => {
    app.use(FAMiddleware);
  });

app.use(errorHandler());

module.exports = app;

package.json

{
  "name": "backend",
  "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.16.1",
    "express-jwt": "6.1.2",
    "express-validator": "^6.14.2",
    "forest-express-sequelize": "8.5.11",
    "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"
  }
}

docker-compose.yml

version: '3.8'

services:
  mysqldb:
    platform: linux/x86_64
    image: mysql:8.0.29
    restart: unless-stopped
    env_file: ./.env
    environment:
      - MYSQL_ROOT_PASSWORD=$MYSQLDB_ROOT_PASSWORD
      - MYSQL_DATABASE=$MYSQLDB_DATABASE
    ports:
      - $MYSQLDB_LOCAL_PORT:$MYSQLDB_DOCKER_PORT
    volumes:
      - db:/var/lib/mysql
  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    ports:
      - 11819:80
    depends_on:
      - mysqldb
    environment:
      PMA_HOSTS: mysqldb
      PMA_USER: $MYSQLDB_USER
      PMA_PASSWORD: $MYSQLDB_ROOT_PASSWORD
      UPLOAD_LIMIT: 100M
  app:
    depends_on:
      - mysqldb
    build: 
      context: .
      network: host
    restart: unless-stopped
    env_file: ./.env
    volumes:
      - .:/var/www/app
    working_dir: /var/www/app
    ports:
      - $NODE_LOCAL_PORT:$NODE_DOCKER_PORT
    environment:
      - DB_HOST=mysqldb
      - DB_USER=$MYSQLDB_USER
      - DB_PASSWORD=$MYSQLDB_ROOT_PASSWORD
      - DB_NAME=$MYSQLDB_DATABASE
      - DB_PORT=$MYSQLDB_DOCKER_PORT
    stdin_open: true
    tty: true
volumes: 
  db:

The Node.js project is running on localhost:6868 without any issues:

enter image description here

terminal log

andand@And-MacBook-Pro backend % docker-compose up --build
[+] Building 42.5s (10/10) FINISHED                                                                                 
 => [internal] load build definition from Dockerfile                                                           0.0s
 => => transferring dockerfile: 32B                                                                            0.0s
 => [internal] load .dockerignore                                                                              0.0s
 => => transferring context: 2B                                                                                0.0s
 => [internal] load metadata for docker.io/library/node:16                                                     0.6s
 => [internal] load build context                                                                              0.5s
 => => transferring context: 2.11MB                                                                            0.5s
 => CACHED [1/6] FROM docker.io/library/node:16@sha256:1ed1e17ccabb09038cfb8a965337ebcda51ef9e9d32082164c502d  0.0s
 => [2/6] COPY package.json .                                                                                  0.0s
 => [3/6] RUN npm install -g nodemon                                                                           3.6s
 => [4/6] RUN npm install                                                                                     35.6s
 => [5/6] COPY . .                                                                                             1.0s 
 => exporting to image                                                                                         1.1s 
 => => exporting layers                                                                                        1.1s 
 => => writing image sha256:53f94f35be4528c5cd899f5702e433acf915a3b1d01a2dbdf098d35482a42bf5                   0.0s 
 => => naming to docker.io/library/backend_app                                                             0.0s 
                                                                                                                    
Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
[+] Running 3/3
 ⠿ Container backend-mysqldb-1     Created                                                                 0.0s
 ⠿ Container backend-app-1         Recreated                                                               0.1s
 ⠿ Container backend-phpmyadmin-1  Created                                                                 0.0s
Attaching to backend-app-1, backend-mysqldb-1, backend-phpmyadmin-1
backend-mysqldb-1     | 2022-08-09 14:22:50+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.29-1.el8 started.
backend-phpmyadmin-1  | AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 172.18.0.3. Set the 'ServerName' directive globally to suppress this message
backend-phpmyadmin-1  | AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 172.18.0.3. Set the 'ServerName' directive globally to suppress this message
backend-phpmyadmin-1  | [Tue Aug 09 14:22:51.704530 2022] [mpm_prefork:notice] [pid 1] AH00163: Apache/2.4.53 (Debian) PHP/8.0.19 configured -- resuming normal operations
backend-phpmyadmin-1  | [Tue Aug 09 14:22:51.706056 2022] [core:notice] [pid 1] AH00094: Command line: 'apache2 -D FOREGROUND'
backend-mysqldb-1     | 2022-08-09 14:22:51+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'
backend-app-1         | [nodemon] 2.0.19
backend-app-1         | [nodemon] to restart at any time, enter `rs`
backend-app-1         | [nodemon] watching path(s): *.*
backend-app-1         | [nodemon] watching extensions: js,mjs,json
backend-app-1         | [nodemon] starting `node /var/www/app/app.js npm run start_dev`
backend-mysqldb-1     | 2022-08-09 14:22:51+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.29-1.el8 started.
backend-mysqldb-1     | '/var/lib/mysql/mysql.sock' -> '/var/run/mysqld/mysqld.sock'
backend-mysqldb-1     | 2022-08-09T14:22:53.698645Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.29) starting as process 1
backend-mysqldb-1     | 2022-08-09T14:22:53.808611Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
backend-mysqldb-1     | 2022-08-09T14:22:53.846153Z 1 [ERROR] [MY-012585] [InnoDB] Linux Native AIO interface is not supported on this platform. Please check your OS documentation and install appropriate binary of InnoDB.
backend-mysqldb-1     | 2022-08-09T14:22:53.846650Z 1 [Warning] [MY-012654] [InnoDB] Linux Native AIO disabled.
backend-mysqldb-1     | 2022-08-09T14:22:54.473510Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
backend-mysqldb-1     | 2022-08-09T14:22:55.462049Z 0 [System] [MY-010229] [Server] Starting XA crash recovery...
backend-mysqldb-1     | 2022-08-09T14:22:55.478820Z 0 [System] [MY-010232] [Server] XA crash recovery finished.
backend-mysqldb-1     | 2022-08-09T14:22:55.754773Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
backend-mysqldb-1     | 2022-08-09T14:22:55.756819Z 0 [System] [MY-013602] [Server] Channel mysql_main configured to support TLS. Encrypted connections are now supported for this channel.
backend-mysqldb-1     | 2022-08-09T14:22:55.774738Z 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.
backend-mysqldb-1     | 2022-08-09T14:22:56.089136Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Bind-address: '::' port: 33060, socket: /var/run/mysqld/mysqlx.sock
backend-mysqldb-1     | 2022-08-09T14:22:56.091257Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.29'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server - GPL.
backend-app-1         | [forest] 🌳🌳🌳  Your configDir ("/var/www/app/forest") does not exist. Please make sure it is set correctly.
backend-app-1         | Server is running on port 8080.
backend-app-1         | [forest] 🌳🌳🌳  Checking need for apimap update...
backend-app-1         | Executing (default): SELECT 1+1 AS result
backend-app-1         | Connection has been established successfully.
backend-app-1         | [forest] 🌳🌳🌳  No change in apimap, nothing sent to Forest.
backend-app-1         | POST /forest/authentication 404 315.425 ms - 1916
backend-app-1         | GET / 304 16.272 ms - -
backend-app-1         | GET /stylesheets/style.css 304 2.196 ms - -

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;

I tried to change:

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'),
  })
);

to this

const watchRouter = new express.Router();

watchRouter.post(
  '/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('/forest', watchRouter);

but it didn't help. I am still getting POST /forest/authentication 404.


Solution

  • I changed the code to this and it's working now:

    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();
    
    // 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;