diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy-vercel.yml similarity index 60% rename from .github/workflows/deploy.yml rename to .github/workflows/deploy-vercel.yml index cb90dec..8867d9c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy-vercel.yml @@ -1,5 +1,9 @@ name: Deploy to Production Environment +# This workflow deploys the backend, a full-server express app to Vercel +# as opposed to their recommended approach of deploying serverless functions +# using NextJS "pages/api". Vercel CLI v28.2.0 is used for deployment + # This workflow will trigger on any tag/release created on *any* branch # Make sure to create tags/releases only from the "master" branch for consistency on: @@ -60,7 +64,7 @@ jobs: npm install npm run lint - deploy-docs: + deploy-client: name: Deploy client to Github Pages needs: lint-client runs-on: ubuntu-latest @@ -77,3 +81,37 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./ publish_branch: gh-pages + + deploy-server: + name: Deploy Server to Vercel + needs: lint-server + runs-on: ubuntu-latest + env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + steps: + - name: Checkout the repository + uses: actions/checkout@v3 + - name: Use NodeJS v16.14.2 + uses: actions/setup-node@v3 + with: + node-version: 16.14.2 + - name: Install Vercel CLI + run: npm install --global vercel@28.2.0 + - name: Pull Vercel Environment Information + run: | + cd server + vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} + - name: Build Project Artifacts + run: | + cd server + vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} + - name: Deploy Project Artifacts to Vercel + run: | + cd server + vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }} + - name: Post Deployment Clean-up + run: | + cd server + rm -r -f .vercel diff --git a/.gitignore b/.gitignore index 5a37509..8b12bc1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ node_modules/ .vscode .env *.zip + +.vercel + +.vercel diff --git a/server/.env.example b/server/.env.example index 1ddb490..cd01f64 100644 --- a/server/.env.example +++ b/server/.env.example @@ -1,3 +1,6 @@ ALLOWED_ORIGINS=http://localhost:3000 ALLOW_CORS=0 +API_RATE_LIMIT=100 +API_WINDOW_MS_MINUTES=15 MONGO_URI=mongodb://localhost/todo-next +DEPLOYMENT_PLATFORM=regular diff --git a/server/.gitignore b/server/.gitignore index d767605..fdf1d5d 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -1,3 +1,5 @@ node_modules/ .env *.zip + +.vercel diff --git a/server/.vercelignore b/server/.vercelignore new file mode 100644 index 0000000..92eb3ee --- /dev/null +++ b/server/.vercelignore @@ -0,0 +1,15 @@ +node_modules/ +*.env +*.zip +*.md +*.toml + +.vercel +.vscode +.git +.github +.eslintrc.js +.eslintignore +.vercelignore +package.json +package-lock.json diff --git a/server/README.md b/server/README.md index a0c2219..607df92 100644 --- a/server/README.md +++ b/server/README.md @@ -38,11 +38,14 @@ The following dependencies are used for this project's localhost development env 3. Set up the environment variables. Create a `.env `file inside the **/server** directory with reference to the `.env.example` file.
- | Variable Name | Description | - | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | MONGO_URI | MongoDB connection string.
Default value uses the localhost MongoDB connection string. | - | ALLOWED_ORIGINS | IP/domain origins in comma-separated values that are allowed to access the API if `ALLOW_CORS=1`.
Include `http://localhost:3000` by default to allow CORS access to the **/client** app. | - | ALLOW_CORS | Allow Cross-Origin Resource Sharing (CORS) on the API endpoints.

Default value is `1`, allowing access to domains listed in `ALLOWED_ORIGINS`.
Setting to `0` will make all endpoints accept requests from all domains, including Postman. | + | Variable Name | Description | + | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | API_WINDOW_MS_MINUTES | Time in `minutes` where `API_RATE_LIMIT` times of successive calls from an IP are allowed on the server. | + | API_RATE_LIMIT | It's the maximum number of allowed API requests on the server per `API_WINDOW_MS_MINUTES`.
Users will receive a `429 - Too many requests` server error after hitting the limit.
The limit will reset after API_WINDOW_MS_MINUTES minutes, after which users can resume making API requests. | + | ALLOW_CORS | Allow Cross-Origin Resource Sharing (CORS) on the API endpoints.

Default value is `1`, allowing access to domains listed in `ALLOWED_ORIGINS`.
Setting to `0` will make all endpoints accept requests from all domains, including Postman. | + | ALLOWED_ORIGINS | IP/domain origins in comma-separated values that are allowed to access the API if `ALLOW_CORS=1`.
Include `http://localhost:3000` by default to allow CORS access to the **/client** app. | + | DEPLOYMENT_PLATFORM | This variable refers to the backend `server`'s hosting platform, defaulting to `DEPLOYMENT_PLATFORM=regular`
for full-server NodeJS express apps.

Valid values are:
`regular` - for traditional full-server NodeJS express apps
`vercel` - for Vercel (serverless) | + | MONGO_URI | MongoDB connection string.
Default value uses the localhost MongoDB connection string. | ## Usage diff --git a/server/api/README.md b/server/api/README.md new file mode 100644 index 0000000..68ddd02 --- /dev/null +++ b/server/api/README.md @@ -0,0 +1,13 @@ +## api + +The `/api` directory is required by vercel when using the @latest `vercel.json` configuration file settings (for Vercel CLI v28.2.0 as of this writing). + +Ideally, this directory should contain **serverless functions** [written per file](https://vercel.com/guides/using-express-with-vercel#next.js) following the NextJS api pages writing guide, but we'd like to deploy a standalone full-server express app on vercel. + +### References + +[[1]](https://vercel.com/guides/using-express-with-vercel) - using express with vercel
+[[2]](https://vercel.com/docs/project-configuration#project-configuration/functions) - project config with vercel.json + +@weaponsforge
+20220828 diff --git a/server/api/index.js b/server/api/index.js new file mode 100644 index 0000000..9dcb0e9 --- /dev/null +++ b/server/api/index.js @@ -0,0 +1,4 @@ +require('dotenv').config() +const app = require('../src/index') + +module.exports = app diff --git a/server/package-lock.json b/server/package-lock.json index 6497756..7a597be 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -13,6 +13,7 @@ "cors": "^2.8.5", "dotenv": "^16.0.1", "express": "^4.18.1", + "express-rate-limit": "^6.5.2", "mongoose": "^6.5.2" }, "devDependencies": { @@ -1385,6 +1386,17 @@ "node": ">= 0.10.0" } }, + "node_modules/express-rate-limit": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.5.2.tgz", + "integrity": "sha512-N0cG/5ccbXfNC+FxRu7ujm2HjKkygF2PL7KLAf/hct9uqKB5QkZVizb/hEst6tUBXnfhblYWgOorN2eY+Saerw==", + "engines": { + "node": ">= 12.9.0" + }, + "peerDependencies": { + "express": "^4 || ^5" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4389,6 +4401,12 @@ "vary": "~1.1.2" } }, + "express-rate-limit": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.5.2.tgz", + "integrity": "sha512-N0cG/5ccbXfNC+FxRu7ujm2HjKkygF2PL7KLAf/hct9uqKB5QkZVizb/hEst6tUBXnfhblYWgOorN2eY+Saerw==", + "requires": {} + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/server/package.json b/server/package.json index 9cc1ab6..ff1bc67 100644 --- a/server/package.json +++ b/server/package.json @@ -2,9 +2,10 @@ "name": "server", "version": "1.0.0", "description": "This directory will contain the backend express server that will serve the Todo CRUD API endpoints.", - "main": "src/index.js", + "main": "api/index.js", "scripts": { - "start": "node src/index.js", + "start": "node api/index.js", + "start:vercel": "node api/index.js", "dev": "nodemon watch src/index.js", "lint": "eslint src", "lint:fix": "eslint src --fix" @@ -16,6 +17,7 @@ "cors": "^2.8.5", "dotenv": "^16.0.1", "express": "^4.18.1", + "express-rate-limit": "^6.5.2", "mongoose": "^6.5.2" }, "devDependencies": { diff --git a/server/public/401.html b/server/public/401.html new file mode 100644 index 0000000..07d5cd3 --- /dev/null +++ b/server/public/401.html @@ -0,0 +1,10 @@ + + + + + 401 + + +

Unauthorized

+ + diff --git a/server/public/404.html b/server/public/404.html new file mode 100644 index 0000000..703c88c --- /dev/null +++ b/server/public/404.html @@ -0,0 +1,10 @@ + + + + + 404 + + +

404 Not Found

+ + diff --git a/server/public/index.html b/server/public/index.html new file mode 100644 index 0000000..5353a82 --- /dev/null +++ b/server/public/index.html @@ -0,0 +1,10 @@ + + + + + Todo Notes API + + +

Welcome to the Todo Notes API v1

+ + diff --git a/server/src/index.js b/server/src/index.js index cc735d5..2b3eae3 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -4,12 +4,19 @@ require('./utils/db') const express = require('express') const cors = require('cors') const cookieParser = require('cookie-parser') +const rateLimit = require('express-rate-limit') const app = express() const PORT = process.env.PORT || 3001 const { corsOptions } = require('./utils/cors_options') const controllers = require('./controllers') +const limiter = rateLimit({ + windowMs: process.env.API_WINDOW_MS_MINUTES * 60 * 1000, // in minutes + max: process.env.API_RATE_LIMIT, // limit each IP to API_RATE_LIMIT requests per windowMs + message: 'Too many requests from this IP. Please try again after 15 minutes.' +}) + // Initialize the express app app.use(express.json()) app.use(express.urlencoded({ extended: false })) @@ -19,7 +26,7 @@ if (process.env.ALLOW_CORS === '1') { app.use(cors(corsOptions)) } -app.use('/api', controllers) +app.use('/api', limiter, controllers) app.get('*', (req, res) => { return res.status(200).send('Welcome to the Todo API') @@ -29,6 +36,10 @@ app.use((err, req, res, next) => { return res.status(500).send(err.message) }) -app.listen(PORT, () => { - console.log(`listening on http://localhost:${PORT}`) -}) +if (process.env.DEPLOYMENT_PLATFORM !== 'vercel') { + app.listen(PORT, () => { + console.log(`listening on http://localhost:${PORT}`) + }) +} + +module.exports = app diff --git a/server/vercel.json b/server/vercel.json new file mode 100644 index 0000000..ff03b2c --- /dev/null +++ b/server/vercel.json @@ -0,0 +1,14 @@ +{ + "rewrites": [ + { + "source": "/api/(.*)", + "destination": "/api" + } + ], + "redirects": [ + { + "source": "/src(.*)", + "destination": "/public/401.html" + } + ] +}