diff --git a/.DS_Store b/.DS_Store index e6a5342f..74b83a3f 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/backend/app.js b/backend/app.js index 222879bf..dc302b55 100644 --- a/backend/app.js +++ b/backend/app.js @@ -28,17 +28,36 @@ const io = new Server(server, { } }); +//cors configuration if (process.env.NODE_ENV === 'production') { app.use(enforce.HTTPS({ trustProtoHeader: true })); const corsOptions = { origin: [ 'https://www.study-compass.com', 'https://studycompass.com', - `http://${process.env.EDUREKA_IP}:${process.env.EDUREKA_PORT}` + // `http://${process.env.EDUREKA_IP}:${process.env.EDUREKA_PORT}` ], optionsSuccessStatus: 200 // for legacy browser support }; - app.use(cors(corsOptions)); + + //apply CORS policy to all routes EXCEPT /api routes + app.use((req, res, next) => { + if (req.path.startsWith('/api')) { + return next(); + } + return cors(corsOptions)(req, res, next); + }); +} else { + //in development, use a more permissive CORS policy for non-API routes + app.use((req, res, next) => { + if (req.path.startsWith('/api')) { + return next(); + } + return cors({ + origin: true, //allow all origins in development + credentials: true + })(req, res, next); + }); } // Other middleware @@ -68,6 +87,8 @@ app.use(async (req, res, next) => { res.status(500).send('Database connection error'); } }); +// berkeley.study-compass.com + const upload = multer({ storage: multer.memoryStorage(), @@ -87,22 +108,22 @@ const ratingRoutes = require('./routes/ratingRoutes.js'); const searchRoutes = require('./routes/searchRoutes.js'); const eventRoutes = require('./routes/eventRoutes.js'); const oieRoutes = require('./routes/oie-routes.js'); +const apiRoutes = require('./routes/apiRoutes.js'); //Added Pk ERROR const orgRoutes = require('./routes/orgRoutes.js'); const workflowRoutes = require('./routes/workflowRoutes.js'); +// Mount routes app.use(authRoutes); app.use(dataRoutes); app.use(friendRoutes); app.use(userRoutes); app.use(analyticsRoutes); -app.use(eventRoutes); - app.use(classroomChangeRoutes); app.use(ratingRoutes); app.use(searchRoutes); - app.use(eventRoutes); app.use(oieRoutes); +app.use('/api', apiRoutes); // API routes with their own CORS configuration app.use(orgRoutes); app.use(workflowRoutes); diff --git a/backend/middlewares/apiCors.js b/backend/middlewares/apiCors.js new file mode 100644 index 00000000..05345204 --- /dev/null +++ b/backend/middlewares/apiCors.js @@ -0,0 +1,31 @@ +const cors = require('cors'); + +/** + * CORS middleware specifically for API routes + * This allows cross-origin requests to API endpoints while maintaining security through API keys + */ +const apiCors = cors({ + origin: '*', + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'], + + allowedHeaders: [ + 'Content-Type', + 'Authorization', + 'X-Requested-With', + 'Accept', + 'Origin', + 'x-api-key' + ], + + //set credentials to false since we're using '*' for origin + credentials: false, + + //set how long the results of a preflight request can be cached + maxAge: 86400, // 24 hours + + //enable preflight requests + preflightContinue: false, + optionsSuccessStatus: 204 +}); + +module.exports = apiCors; \ No newline at end of file diff --git a/backend/middlewares/apiKeyMiddleware.js b/backend/middlewares/apiKeyMiddleware.js new file mode 100644 index 00000000..077f7faa --- /dev/null +++ b/backend/middlewares/apiKeyMiddleware.js @@ -0,0 +1,42 @@ +const mongoose = require('mongoose'); +const getModels = require('../services/getModelService.js'); + +const apiKeyMiddleware = async (req, res, next) => { + try { + const apiKey = req.headers['x-api-key']; + + if (!apiKey) { + return res.status(401).json({ error: 'API key is required' }); + } + + const { Api } = getModels(req, 'Api'); + const apiKeyData = await Api.findOne({ api_key: apiKey }); + + if (!apiKeyData) { + return res.status(401).json({ error: 'Invalid API key' }); + } + + //check if API key is expired + if (apiKeyData.expiresAt && new Date() > apiKeyData.expiresAt) { + return res.status(401).json({ error: 'API key has expired' }); + } + + //check IP whitelist if configured + if (apiKeyData.allowedIPs && apiKeyData.allowedIPs.length > 0) { + const clientIP = req.ip; + if (!apiKeyData.allowedIPs.includes(clientIP)) { + return res.status(403).json({ error: 'IP not whitelisted' }); + } + } + + //attach API key data to request for use in other middleware + req.apiKeyData = apiKeyData; + next(); + } catch (error) { + console.error('API Key Middleware Error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +module.exports = { apiKeyMiddleware }; + diff --git a/backend/middlewares/rateLimit.js b/backend/middlewares/rateLimit.js new file mode 100644 index 00000000..07c60e0f --- /dev/null +++ b/backend/middlewares/rateLimit.js @@ -0,0 +1,69 @@ + //Rate Limiter + +const rateLimit = require('express-rate-limit'); + +const coolDown = 15 * 60 * 1000; // 15 minutes + +const limiter = (options = {}) => { + const { + maxRequests = 200, + windowMs = coolDown, // 15 minutes + message = 'Too many requests, please try again later.', + keyGenerator = (req) => { + //use API key if available, otherwise fall back to IP + return req.apiKeyData ? req.apiKeyData.api_key : req.ip; + }, + skip = (req) => false, + handler = (req, res) => { + res.status(429).json({ + error: message, + requestId: req.requestId, + retryAfter: Math.ceil(windowMs / 1000) + }); + }, + //debug option to help troubleshoot rate limiting + debug = process.env.NODE_ENV === 'development' + } = options; + + return rateLimit({ + windowMs, + max: maxRequests, + message, + standardHeaders: true, + legacyHeaders: false, + keyGenerator, + skip, + handler, + //custom headers + headers: true, + skipFailedRequests: false, + skipSuccessfulRequests: false, + statusCode: 429, + skip: (req) => { + //skip rate limiting for certain paths or conditions + //examples below + // if (req.path.startsWith('/health')) return true; + // if (req.path.startsWith('/metrics')) return true; + return false; + }, + debug + }); +}; + +module.exports = { + limiter, + //strict rate limiter for sensitive endpoints + strictLimiter: (maxRequests = 50) => limiter({ + maxRequests, + windowMs: coolDown, // 15 minutes + message: 'Rate limit exceeded. Please try again in a minute.' + }), + //burst rate limiter for high-traffic endpoints + burstLimiter: (maxRequests = 1000) => limiter({ + maxRequests, + windowMs: coolDown, // 15 minutes + message: 'Too many requests in a short time. Please try again in a minute.' + }), + //default rate limiter + defaultLimiter: limiter +}; \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index cd649e3d..aeca5ada 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -20,11 +20,13 @@ "date-fns": "^4.1.0", "dotenv": "^16.3.2", "express": "^4.18.2", + "express-rate-limit": "^7.5.0", "express-sslify": "^1.2.0", "google-auth-library": "^9.4.2", "googleapis": "^131.0.0", "jsonwebtoken": "^9.0.2", "mongoose": "^8.0.3", + "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", "node-cron": "^3.0.3", "nodemon": "^3.1.7", @@ -698,6 +700,24 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/bcrypt": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", @@ -1461,6 +1481,21 @@ "node": ">= 0.10.0" } }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, "node_modules/express-sslify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/express-sslify/-/express-sslify-1.2.0.tgz", @@ -2706,6 +2741,34 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/mpath": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", @@ -2959,6 +3022,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", diff --git a/backend/package.json b/backend/package.json index 940f9508..3ec7db0d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,11 +11,13 @@ "date-fns": "^4.1.0", "dotenv": "^16.3.2", "express": "^4.18.2", + "express-rate-limit": "^7.5.0", "express-sslify": "^1.2.0", "google-auth-library": "^9.4.2", "googleapis": "^131.0.0", "jsonwebtoken": "^9.0.2", "mongoose": "^8.0.3", + "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", "node-cron": "^3.0.3", "nodemon": "^3.1.7", diff --git a/backend/routes/apiRoutes.js b/backend/routes/apiRoutes.js new file mode 100644 index 00000000..cde0e364 --- /dev/null +++ b/backend/routes/apiRoutes.js @@ -0,0 +1,270 @@ +const express = require('express'); +const router = express.Router(); +const { limiter, strictLimiter, burstLimiter, defaultLimiter } = require('../middlewares/rateLimit.js'); // Rate limiting middleware +const {apiKeyMiddleware} = require('../middlewares/apiKeyMiddleware.js'); // API key validation +const apiCors = require('../middlewares/apiCors.js'); // API-specific CORS middleware +const crypto = require('crypto'); // For generating API keys +const mongoose = require('mongoose'); +const {verifyToken}= require('../middlewares/verifyToken'); +const getModels = require('../services/getModelService.js'); +require('dotenv').config(); + +// Apply CORS to all API routes +router.use(apiCors); + +// Generate a new API key +router.post('/create_api', verifyToken, async (req, res) => { + try { + const { Api } = getModels(req, 'Api'); + //Add a set authorization here, just a tag to connect to api key front end will handle assigning + //Authorized or Unauthorized try both + + const userId = req.user.userId; + const user_status = req.headers["Authorization"];// see if this works + + // Generate API key and verify user does not have any pre-existing one + const existingApi = await Api.findOne({ owner: userId }); + if (existingApi) { + console.log ('POST: /create_api failed. User already has an API key') + return res.status(400).json({ success: false, message: 'User already has an API key.' }); + } + const apiKey = crypto.randomBytes(32).toString('hex'); + + // Create and save the new API key entry + const newApi = new Api({ + api_key: apiKey, + owner: userId, + Authorization: "Unauthorized", // see if this works + description: req.body.description || 'API key created via dashboard', + scopes: req.body.scopes || ['read'], + expiresAt: req.body.expiresAt ? new Date(req.body.expiresAt) : null, + allowedIPs: req.body.allowedIPs || [] + }); + + await newApi.save(); + + console.log('POST: /create_api successful. API key generated:', newApi); + res.status(201).json({success: true, message: 'API key generated successfully.', apiKey: newApi }); + } catch (error) { + console.error('POST: /create_api failed. Error:', error); + res.status(500).json({ sucess: false, message: 'Unable to generate api' }); + } +}); + +//router.use('/protected', apiKeyMiddleware); // ANY LINE THAT CONTAINS THE /protected will apply apiKeyMiddleware too + +//Simplistic then detailed dont stress about key things +//If we want to grant access to a route, we want to grant access using the apiKeyMiddleware +//apiKeyMiddleware will guarantee and verify the existence of the key and then continue to grab items + +//Api key adds other servers to use our routes, functionallity isnt to have to make a verify token +//Instead of checking user we should be checking the api key, trust any request with a valid api key, take out verifyToken, + + +//Verify the api key, have a special header for api keys, when header is present should check validity of the api key, then pass them through rate limiters +//this is the only one that takes outside servers that not our front, prolly only given to none verify token routes, change user validity to api valdity + +//associating a header : with an api key + + + +/* +Tasks to do: +find a way to test the request coming from a different server (new api with a new routing system +): create a new repositiory, a light weight server to use fetch or axios to call routes from a different server (dont know the logistics) +Set up one route/action + +calling a different api to another server + + +- change validity from user, to strictly api key validation- make sure exists and cant be looped around +- rate limiting authority verification | associate the api in the create function with a tag based on their user id, would need to be assigned that tag from front end, but have it in schema +//so thunder clients say "Authorization:" unauthorized_org , "apikey" +-api caller program + +3/11/25 +Changed interface and teach new syntax +Every route where i call a database route +small change or medium change +*/ + + + +router.get('/details', burstLimiter(), apiKeyMiddleware, async (req, res, next) => { + const apiKey = req.headers['x-api-key']; + + try { + const apiKeyData = req.apiKeyData; + if (!apiKeyData) { + return res.status(404).json({ error: 'API key not found' }); + } + + console.log('GET: /details successful. API key details:', apiKeyData); + return res.status(200).json(apiKeyData); + } catch (error) { + console.error('GET: /details failed. Error:', error); + return next(error); + } +}); + +// Delete an API key, Verify user is the owner +router.delete('/delete-api', verifyToken, apiKeyMiddleware, async (req, res) => { + const userId = req.user.userId; + try { + const { Api } = getModels(req, 'Api'); + const deletedApi = await Api.findOneAndDelete({ owner: userId }); + + if (!deletedApi) { + console.error('API key not found for deletion'); + return res.status(404).json({ error: 'API key not found.' }); + } + + console.log('DELETE: /delete successful. Deleted API key:', deletedApi); + res.status(200).json({ message: 'API key deleted successfully.' }); + } catch (error) { + console.error('DELETE: /delete failed. Error:', error); + res.status(500).json({success:false, message : 'Error deleting API.' }); + } +}); + +//temporary logic just to test connection +function checkApiKey(req, res, next) { + const apiKey = req.headers['x-api-key']; + if (apiKey === process.env.EDUREKA_API) { + return next(); + } else { + return res.status(403).json({ error: 'Forbidden: Invalid API key' }); + } +} + +// Use different rate limiters for different endpoints +router.get('/events', defaultLimiter(), apiKeyMiddleware, async (req, res) => { + + try { + const { Event } = getModels(req, 'Event'); + const events = await Event.find({}); + + console.log('GET: /api/events successful. Events:', events); + res.status(200).json(events); + } catch (error) { + console.error('GET: /api/events failed. Error:', error); + res.status(500).json({ error: 'Unable to retrieve events' }); + } +}); + +//simple post request, returns what it gets +router.post('/test', burstLimiter(), apiKeyMiddleware, async (req, res) => { + console.log('POST: /api/test successful. Request body:', req.body); + res.status(200).json(req.body); +}); + +router.post('/sensitive', strictLimiter(), checkApiKey, async (req, res) => { + try { + // Process sensitive data + console.log('POST: /api/sensitive successful. Processing sensitive data'); + res.status(200).json({ + success: true, + message: 'Sensitive operation completed successfully', + data: req.body + }); + } catch (error) { + console.error('POST: /api/sensitive failed. Error:', error); + res.status(500).json({ error: 'Unable to process sensitive operation' }); + } +}); + +router.post('/rotate-api-key', verifyToken, apiKeyMiddleware, async (req, res) => { + try { + const userId = req.user.userId; + const { Api } = getModels(req, 'Api'); + + //find existing API key + const existingApi = await Api.findOne({ owner: userId }); + if (!existingApi) { + return res.status(404).json({ error: 'API key not found.' }); + } + + //generate new API key + const newApiKey = crypto.randomBytes(32).toString('hex'); + + //update the API key while preserving other properties + existingApi.api_key = newApiKey; + existingApi.usageCount = 0; //reset usage count + existingApi.dailyUsageCount = 0; //reset daily usage count + existingApi.lastUsageReset = new Date(); //reset last usage reset time + + await existingApi.save(); + + console.log('POST: /rotate-api-key successful. API key rotated'); + res.status(200).json({ + success: true, + message: 'API key rotated successfully', + apiKey: existingApi + }); + } catch (error) { + console.error('POST: /rotate-api-key failed. Error:', error); + res.status(500).json({ success: false, message: 'Error rotating API key' }); + } +}); + +//public API endpoint that doesn't require authentication +//testing purposes +// router.get('/public', defaultLimiter(), (req, res) => { +// res.status(200).json({ +// success: true, +// message: 'Public API endpoint accessed successfully', +// timestamp: new Date().toISOString() +// }); +// }); + +//route to reset rate limits for testing purposes, only available in development mode +router.post('/reset-rate-limit', async (req, res) => { + //only allow in development mode + if (process.env.NODE_ENV !== 'development') { + return res.status(403).json({ + success: false, + message: 'Rate limit reset is only available in development mode' + }); + } + + try { + const { Api } = getModels(req, 'Api'); + const apiKey = req.headers['x-api-key']; + + if (!apiKey) { + return res.status(400).json({ + success: false, + message: 'API key is required' + }); + } + + const apiKeyData = await Api.findOne({ api_key: apiKey }); + if (!apiKeyData) { + return res.status(404).json({ + success: false, + message: 'API key not found' + }); + } + + apiKeyData.usageCount = 0; + apiKeyData.dailyUsageCount = 0; + apiKeyData.lastUsageReset = new Date(); + + await apiKeyData.save(); + + console.log('POST: /reset-rate-limit successful. Reset rate limit for API key:', apiKey); + res.status(200).json({ + success: true, + message: 'Rate limit reset successfully', + apiKey: apiKeyData + }); + } catch (error) { + console.error('POST: /reset-rate-limit failed. Error:', error); + res.status(500).json({ + success: false, + message: 'Error resetting rate limit' + }); + } +}); + +module.exports = router; diff --git a/backend/schemas/api.js b/backend/schemas/api.js new file mode 100644 index 00000000..f03da5a7 --- /dev/null +++ b/backend/schemas/api.js @@ -0,0 +1,87 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const apiSchema = new Schema({ + api_key: { + type: String, + required: true, + unique: true, + trim: true, + }, + owner: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + createdAt: { + type: Date, + default: Date.now, + }, + expiresAt: { + type: Date, + default: null, + }, + lastUsed: { + type: Date, + default: null, + }, + dailyUsageCount: { + type: Number, + default: 0, + }, + lastUsageReset: { + type: Date, + default: Date.now, + }, + Authorization: { + type: String, + enum: ["Unauthorized", "Authorized"], + default: "Unauthorized" + }, + allowedIPs: [{ + type: String, + trim: true + }], + description: { + type: String, + trim: true, + maxLength: 500 + }, + isActive: { + type: Boolean, + default: true + }, + scopes: [{ + type: String, + enum: ['read', 'write', 'admin'] + }], + metadata: { + type: Map, + of: Schema.Types.Mixed, + default: {} + } +}); + +// Index for faster queries +apiSchema.index({ api_key: 1 }); +apiSchema.index({ owner: 1 }); +apiSchema.index({ expiresAt: 1 }); +apiSchema.index({ isActive: 1 }); + +// Pre-save middleware to reset daily usage count if needed +apiSchema.pre('save', function(next) { + const now = new Date(); + const lastReset = this.lastUsageReset || new Date(0); + + // Reset daily usage if it's a new day + if (lastReset.getDate() !== now.getDate() || + lastReset.getMonth() !== now.getMonth() || + lastReset.getFullYear() !== now.getFullYear()) { + this.dailyUsageCount = 0; + this.lastUsageReset = now; + } + + next(); +}); + +module.exports = apiSchema \ No newline at end of file diff --git a/backend/services/getModelService.js b/backend/services/getModelService.js index 7d824b93..32a79125 100644 --- a/backend/services/getModelService.js +++ b/backend/services/getModelService.js @@ -18,6 +18,7 @@ const searchSchema = require('../schemas/search'); const studyHistorySchema = require('../schemas/studyHistory'); const userSchema = require('../schemas/user'); const visitSchema = require('../schemas/visit'); +const apiSchema = require('../schemas/api'); //events const approvalFlowDefinition = require('../schemas/events/approvalFlowDefinition'); @@ -45,6 +46,7 @@ const getModels = (req, ...names) => { StudyHistory: req.db.model('StudyHistory', studyHistorySchema, 'studyHistories'), User: req.db.model('User', userSchema, 'users'), Visit: req.db.model('Visit', visitSchema, 'visits'), + Api: req.db.model('Api', apiSchema, 'api'), ApprovalFlow: req.db.model('ApprovalFlow', approvalFlowDefinition, 'approvalFlows'), ApprovalInstance: req.db.model('ApprovalInstance', approvalFlowInstance, 'approvalInstances') }; diff --git a/package-lock.json b/package-lock.json index 6ad88385..a5b7d122 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "dependencies": { "bcrypt": "^5.1.1", "dotenv": "^16.3.1", + "joi": "^17.13.3", "jsonwebtoken": "^9.0.2", "mongoose": "^8.0.4", "react-router-dom": "^6.21.1", @@ -27,6 +28,21 @@ "node": ">=6.9.0" } }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "license": "BSD-3-Clause", @@ -59,6 +75,27 @@ "node": ">=14.0.0" } }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -477,6 +514,19 @@ "node": ">=8" } }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "license": "MIT", @@ -1072,7 +1122,9 @@ } }, "node_modules/tar": { - "version": "6.2.0", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "license": "ISC", "dependencies": { "chownr": "^2.0.0", diff --git a/package.json b/package.json index 1248b299..1adbdafd 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "dependencies": { "bcrypt": "^5.1.1", "dotenv": "^16.3.1", + "joi": "^17.13.3", "jsonwebtoken": "^9.0.2", "mongoose": "^8.0.4", "react-router-dom": "^6.21.1", diff --git a/test-api-client.html b/test-api-client.html new file mode 100644 index 00000000..b9a5c82a --- /dev/null +++ b/test-api-client.html @@ -0,0 +1,211 @@ + + +
+ + +