diff --git a/README.md b/README.md index 95bac75..ce8cab5 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Designed for use on iPads, laptops, or other devices commonly used by students. ## Setup -To run this project locally, you will need to have Node.js installed. The application consists of a backend API and a frontend client, which can be run together with a single command. +To run this project locally, you will need to have Node.js installed. 1. Clone the repository: @@ -55,28 +55,38 @@ cd DigitalBonesBox ``` 2. Install dependencies: -This command will install the necessary packages for both the root project and the boneset-api server. ```bash npm install npm install --prefix boneset-api ``` -3. Run the application: +3. Run the application (recommended — one origin): -To start both the backend API server and the frontend server concurrently, with the server automatically reloaded each time changes are made, run +Express serves both the API and the frontend. Open [http://127.0.0.1:8000/](http://127.0.0.1:8000/). ```bash -npm run dev +npm start --prefix boneset-api ``` -Or, to run without live server reloading, run +Alternatively, for API-only development with nodemon: ```bash -npm start +npm run dev:api ``` -Your browser should automatically open to the application. +The older dual-process scripts (`npm start` / `npm run dev`) still start Express plus `http-server`, but the UI must be opened via the Express origin (`http://127.0.0.1:8000/`) because API calls use same-origin relative URLs. + +## Deploy (Vercel) + +The app deploys through a single Express entrypoint at `api/index.js` (see `vercel.json`). + +- Preview: from a linked Git branch or `npx vercel` +- Production: `npx vercel --prod` or merge to the production branch after the Vercel project is connected + +Static UI assets and bone images are copied into `public/` during install so Vercel can serve them on the CDN. + +No application environment variables are required for the current build. ## Contributing diff --git a/api/index.js b/api/index.js new file mode 100644 index 0000000..d411d3c --- /dev/null +++ b/api/index.js @@ -0,0 +1,6 @@ +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const { app } = require("../boneset-api/server.js"); + +export default app; diff --git a/boneset-api/package.json b/boneset-api/package.json index f3506f5..579ba76 100644 --- a/boneset-api/package.json +++ b/boneset-api/package.json @@ -4,6 +4,7 @@ "description": "", "main": "server.js", "scripts": { + "start": "node server.js", "test": "echo \"Error: no test specified\" && exit 1", "dev": "nodemon" }, diff --git a/boneset-api/server.js b/boneset-api/server.js index 0de651c..6845e0f 100644 --- a/boneset-api/server.js +++ b/boneset-api/server.js @@ -12,8 +12,6 @@ const PORT = process.env.PORT || 8000; app.use(cors()); app.use(express.json()); -const API_BASE_URL = `http://127.0.0.1:${PORT}`; - const LOCAL_DATA_DIR = path.join(__dirname, "data"); const BONESET_DIR = path.join(LOCAL_DATA_DIR, "boneset"); const BONES_DIR = path.join(LOCAL_DATA_DIR, "bones"); @@ -22,6 +20,7 @@ const TEXT_LABEL_ANNOTATIONS_DIR = path.join(LOCAL_DATA_DIR, "annotations", "tex const ROTATIONS_TEMPLATE_DIR = path.join(LOCAL_DATA_DIR, "annotations", "rotations annotations"); const DESCRIPTIONS_DIR = path.join(LOCAL_DATA_DIR, "descriptions"); const IMAGES_DIR = path.join(LOCAL_DATA_DIR, "images"); +const TEMPLATES_DIR = path.join(__dirname, "../templates"); const BONESET_NAMES = ["bony_pelvis", "skull", "thorax", "vertebrae", "upper_limb", "lower_limb"]; @@ -183,7 +182,7 @@ function searchItems(query, limit = 20) { } // Routes -app.get("/", (_req, res) => { +app.get("/health", (_req, res) => { res.json({ message: "Welcome to the Boneset API" }); }); @@ -312,7 +311,7 @@ app.get("/api/bone-data/", async (req, res) => { const imagesArray = descriptionData.images || []; const images = imagesArray.map((filename) => ({ filename, - url: `${API_BASE_URL}/api/images/${encodeURIComponent(filename)}`, + url: `/api/images/${encodeURIComponent(filename)}`, })); res.json({ @@ -465,6 +464,13 @@ app.get("/api/search", searchLimiter, (req, res) => { } }); +// Serve the frontend from the same origin as the API. +app.use(express.static(TEMPLATES_DIR)); + +app.get("/", (_req, res) => { + res.sendFile(path.join(TEMPLATES_DIR, "boneset.html")); +}); + async function startServer() { await initializeSearchCache(); diff --git a/boneset-api/server.test.js b/boneset-api/server.test.js index 116dd48..b179ce6 100644 --- a/boneset-api/server.test.js +++ b/boneset-api/server.test.js @@ -2,15 +2,22 @@ const request = require("supertest"); const { app } = require("./server"); describe("Initial configuration tests 263", () => { - it("should return 200 OK", async () => { - const response = await request(app).get("/"); + it("should return 200 OK from health", async () => { + const response = await request(app).get("/health"); expect(response.statusCode).toBe(200); }); - it("should return message welcome", async () => { - const response = await request(app).get("/"); + it("should return message welcome from health", async () => { + const response = await request(app).get("/health"); expect(response.body.message).toBe("Welcome to the Boneset API"); }); + + it("should serve the app HTML at /", async () => { + const response = await request(app).get("/"); + expect(response.statusCode).toBe(200); + expect(response.headers["content-type"]).toMatch(/html/); + expect(response.text).toContain("Digital Bone Box"); + }); }); // Unit tests for Issue 267: GET /api/annotations/:boneId diff --git a/package-lock.json b/package-lock.json index bb8134b..7748005 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "body-parser": "^1.20.6", "cors": "^2.8.5", "express": "^4.22.2", + "express-rate-limit": "^8.7.0", "simple-git": "^3.36.0" }, "devDependencies": { @@ -4749,6 +4750,48 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz", + "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express-rate-limit/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/express-rate-limit/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5403,6 +5446,15 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ip-address": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz", + "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", diff --git a/package.json b/package.json index 7c703b5..4845ad6 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "body-parser": "^1.20.6", "cors": "^2.8.5", "express": "^4.22.2", + "express-rate-limit": "^8.7.0", "simple-git": "^3.36.0" }, "devDependencies": { diff --git a/templates/boneset.html b/templates/boneset.html index 18e70a3..7030cde 100644 --- a/templates/boneset.html +++ b/templates/boneset.html @@ -81,7 +81,7 @@