Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/update nces fetching #398

Merged
merged 4 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
178 changes: 107 additions & 71 deletions app/server/app/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
require("dotenv").config();

const { resolve } = require("node:path");
const { readFile } = require("node:fs/promises");
const express = require("express");
const axios = require("axios").default || require("axios"); // TODO: https://github.com/axios/axios/issues/5011
const cors = require("cors");
const helmet = require("helmet");
const morgan = require("morgan");
Expand All @@ -11,17 +13,12 @@
const errorHandler = require("./utilities/errorHandler");
const log = require("./utilities/logger");
const samlStrategy = require("./config/samlStrategy");
const { s3BucketUrl } = require("./config/s3");
const { protectClientRoutes, checkClientRouteExists } = require("./middleware");
const routes = require("./routes");

const {
NODE_ENV,
PORT,
CLIENT_URL,
SERVER_BASE_PATH,
CLOUD_SPACE,
JSON_PAYLOAD_LIMIT,
} = process.env;
const { NODE_ENV, PORT, CLIENT_URL, SERVER_BASE_PATH, JSON_PAYLOAD_LIMIT } =
process.env;

const requiredEnvironmentVariables = [
"SERVER_URL",
Expand Down Expand Up @@ -62,75 +59,114 @@
}
});

const app = express();
const port = PORT || 3001;

app.use(helmet({ contentSecurityPolicy: false }));
app.use(helmet.hsts({ maxAge: 31536000 }));

/** Instruct web browsers to disable caching. */
app.use((req, res, next) => {
res.setHeader("Surrogate-Control", "no-store");
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate"); // prettier-ignore
res.setHeader("Pragma", "no-cache");
res.setHeader("Expires", "0");
next();
});

app.disable("x-powered-by");

/**
* Enable CORS and logging with morgan for local development only.
* NOTE: process.env.NODE_ENV set to "development" below to match value defined
* in create-react-app when client app is run locally via `npm start`
* Fetch NCES JSON data from S3 bucket or read from local file system.
*/
if (NODE_ENV === "development") {
app.use(cors({ origin: CLIENT_URL, credentials: true }));
app.use(morgan("dev"));
}
function fetchNcesData() {
const localFilePath = resolve(__dirname, "./content", "nces.json");
const s3FileUrl = `${s3BucketUrl}/content/nces.json`;

app.use(express.json({ limit: JSON_PAYLOAD_LIMIT || "5mb" }));
app.use(cookieParser());
app.use(express.urlencoded({ extended: true }));
const logMessage =
NODE_ENV === "development"
? "Reading NCES.json file from disk."
: `Fetching NCES.json from S3 bucket.`;

app.use(passport.initialize());
passport.use("saml", samlStrategy);

/**
* If SERVER_BASE_PATH is provided, serve routes and static files from there
* (e.g. /csb).
*/
const basePath = `${SERVER_BASE_PATH || ""}/`;
app.use(basePath, routes);

/**
* Use regex to add trailing slash on static requests
* (required when using sub path).
*/
const pathRegex = new RegExp(`^\\${SERVER_BASE_PATH || ""}$`);
app.all(pathRegex, (req, res) => res.redirect(`${basePath}`));

/**
* Serve client app's static built files.
* NOTE: client app's `build` directory contents copied into server app's
* `public` directory in CI/CD step.
*/
app.use(basePath, express.static(resolve(__dirname, "public")));

/** Ensure that requested client route exists (otherwise send 404). */
app.use(checkClientRouteExists);
log({ level: "info", message: logMessage });

/** Ensure user is authenticated on all client-side routes except / and /welcome */
app.use(protectClientRoutes);
return Promise.resolve(
/**
* local development: read file directly from disk
* Cloud.gov: fetch file from the public s3 bucket
*/
NODE_ENV === "development"
? readFile(localFilePath, "utf8").then((string) => JSON.parse(string))
: axios.get(s3FileUrl).then((res) => res.data),
).catch((error) => {
const errorStatus = error.response?.status || 500;
const errorMethod = error.response?.config?.method?.toUpperCase();
const errorUrl = error.response?.config?.url;

const logMessage = `S3 Error: ${errorStatus} ${errorMethod} ${errorUrl}`;
log({ level: "error", message: logMessage });

/** Serve client-side routes. */
app.get("*", (req, res) => {
res.sendFile(resolve(__dirname, "public/index.html"));
});
process.exitCode = 1;
});
}

app.use(errorHandler);
fetchNcesData().then((ncesData) => {
const app = express();
const port = PORT || 3001;

/** Store NCES JSON data in the Express app's locals object. */
app.locals.ncesData = ncesData;

app.use(helmet({ contentSecurityPolicy: false }));
app.use(helmet.hsts({ maxAge: 31536000 }));

/** Instruct web browsers to disable caching. */
app.use((req, res, next) => {
res.setHeader("Surrogate-Control", "no-store");
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate"); // prettier-ignore
res.setHeader("Pragma", "no-cache");
res.setHeader("Expires", "0");
next();
});

app.disable("x-powered-by");

/**
* Enable CORS and logging with morgan for local development only.
* NOTE: process.env.NODE_ENV set to "development" below to match value defined
* in create-react-app when client app is run locally via `npm start`
*/
if (NODE_ENV === "development") {
app.use(cors({ origin: CLIENT_URL, credentials: true }));
app.use(morgan("dev"));
}

app.listen(port, () => {
const logMessage = `Server listening on port ${port}`;
log({ level: "info", message: logMessage });
app.use(express.json({ limit: JSON_PAYLOAD_LIMIT || "5mb" }));
app.use(cookieParser());

Check failure

Code scanning / CodeQL

Missing CSRF middleware High

This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a
request handler
without CSRF protection.
This cookie middleware is serving a request handler without CSRF protection.
This cookie middleware is serving a request handler without CSRF protection.
This cookie middleware is serving a request handler without CSRF protection.
This cookie middleware is serving a request handler without CSRF protection.
This cookie middleware is serving a request handler without CSRF protection.
This cookie middleware is serving a request handler without CSRF protection.
This cookie middleware is serving a request handler without CSRF protection.
This cookie middleware is serving a request handler without CSRF protection.
This cookie middleware is serving a request handler without CSRF protection.
This cookie middleware is serving a request handler without CSRF protection.
This cookie middleware is serving a request handler without CSRF protection.
This cookie middleware is serving a request handler without CSRF protection.
This cookie middleware is serving a request handler without CSRF protection.
This cookie middleware is serving a request handler without CSRF protection.
This cookie middleware is serving a request handler without CSRF protection.
app.use(express.urlencoded({ extended: true }));

app.use(passport.initialize());
passport.use("saml", samlStrategy);

/**
* If SERVER_BASE_PATH is provided, serve routes and static files from there
* (e.g. /csb).
*/
const basePath = `${SERVER_BASE_PATH || ""}/`;
app.use(basePath, routes);

/**
* Use regex to add trailing slash on static requests
* (required when using sub path).
*/
const pathRegex = new RegExp(`^\\${SERVER_BASE_PATH || ""}$`);

Check failure

Code scanning / CodeQL

Regular expression injection High

This regular expression is constructed from a
environment variable
.
app.all(pathRegex, (req, res) => res.redirect(`${basePath}`));

/**
* Serve client app's static built files.
* NOTE: client app's `build` directory contents copied into server app's
* `public` directory in CI/CD step.
*/
app.use(basePath, express.static(resolve(__dirname, "public")));

/** Ensure that requested client route exists (otherwise send 404). */
app.use(checkClientRouteExists);

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
a file system access
, but is not rate-limited.

/** Ensure user is authenticated on all client-side routes except / and /welcome */
app.use(protectClientRoutes);

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
This route handler performs
authorization
, but is not rate-limited.

/** Serve client-side routes. */
app.get("*", (req, res) => {
res.sendFile(resolve(__dirname, "public/index.html"));
});
Comment on lines +162 to +164

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
a file system access
, but is not rate-limited.

app.use(errorHandler);

app.listen(port, () => {
const logMessage = `Server listening on port ${port}`;
log({ level: "info", message: logMessage });
});
});
13 changes: 9 additions & 4 deletions app/server/app/routes/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { readFile } = require("node:fs/promises");
const express = require("express");
const axios = require("axios").default || require("axios"); // TODO: https://github.com/axios/axios/issues/5011
// ---
const { s3BucketUrl } = require("../utilities/s3");
const { s3BucketUrl } = require("../config/s3");
const log = require("../utilities/logger");

const { NODE_ENV } = process.env;
Expand Down Expand Up @@ -33,16 +33,21 @@ router.get("/", (req, res) => {
filenames.map((filename) => {
const localFilePath = resolve(__dirname, "../content", filename);
const s3FileUrl = `${s3BucketUrl}/content/${filename}`;
const logMessage = `Fetching ${filename} from S3 bucket.`;

const logMessage =
NODE_ENV === "development"
? `Reading ${filename} file from disk.`
: `Fetching ${filename} from S3 bucket.`;

log({ level: "info", message: logMessage });

/**
* local development: read files directly from disk
* Cloud.gov: fetch files from the public s3 bucket
*/
return NODE_ENV === "development"
? readFile(localFilePath, "utf8")
: (log({ level: "info", message: logMessage, req }),
axios.get(s3FileUrl).then((res) => res.data));
: axios.get(s3FileUrl).then((res) => res.data);
}),
)
.then((data) => {
Expand Down
47 changes: 9 additions & 38 deletions app/server/app/routes/formioNCES.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
const { resolve } = require("node:path");
const { readFile } = require("node:fs/promises");
const express = require("express");
const axios = require("axios").default || require("axios"); // TODO: https://github.com/axios/axios/issues/5011
// ---
const { s3BucketUrl } = require("../utilities/s3");
const log = require("../utilities/logger");

const { NODE_ENV, FORMIO_NCES_API_KEY } = process.env;
const { FORMIO_NCES_API_KEY } = process.env;

const router = express.Router();

Expand Down Expand Up @@ -42,41 +38,16 @@ router.get("/:searchText?", (req, res) => {
return res.json({});
}

const localFilePath = resolve(__dirname, "../content", "nces.json");
const s3FileUrl = `${s3BucketUrl}/content/nces.json`;
const logMessage = `Fetching NCES.json from S3 bucket.`;
const result = req.app.locals.ncesData.find((item) => {
return item["NCES ID"] === searchText;
});

Promise.resolve(
/**
* local development: read file directly from disk
* Cloud.gov: fetch file from the public s3 bucket
*/
NODE_ENV === "development"
? readFile(localFilePath, "utf8").then((string) => JSON.parse(string))
: (log({ level: "info", message: logMessage, req }),
axios.get(s3FileUrl).then((res) => res.data)),
)
.then((data) => {
const result = data.find((item) => item["NCES ID"] === searchText);
const logMessage =
`NCES data searched with NCES ID '${searchText}' resulting in ` +
`${result ? "a match" : "no matches"}.`;
log({ level: "info", message: logMessage, req });

const logMessage =
`NCES data searched with NCES ID '${searchText}' resulting in ` +
`${result ? "a match" : "no matches"}.`;
log({ level: "info", message: logMessage, req });

return res.json({ ...result });
})
.catch((error) => {
const errorStatus = error.response?.status || 500;
const errorMethod = error.response?.config?.method?.toUpperCase();
const errorUrl = error.response?.config?.url;

const logMessage = `S3 Error: ${errorStatus} ${errorMethod} ${errorUrl}`;
log({ level: "error", message: logMessage, req });

const errorMessage = `Error getting NCES.json data from S3 bucket.`;
return res.status(errorStatus).json({ message: errorMessage });
});
return res.json({ ...result });
});

module.exports = router;
2 changes: 1 addition & 1 deletion app/server/app/utilities/bap.js
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ function setupConnection(req) {
const logMessage = `Initializing BAP connection: ${userInfo.url}.`;
log({ level: "info", message: logMessage, req });

/** Store bapConnection in global express object using req.app.locals. */
/** Store BAP connection in the Express app's locals object. */
req.app.locals.bapConnection = bapConnection;
})
.catch((err) => {
Expand Down
1 change: 1 addition & 0 deletions assets/NCES.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions assets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**IMPORTANT:** Do not remove the [`NCES.json`](./NCES.json) file in this directory, as it’s used in the 2022 FRF (fetched from the Formio form definition).
Loading