diff --git a/server/auth.js b/server/auth.js index 9da0213..bf0d6ad 100644 --- a/server/auth.js +++ b/server/auth.js @@ -16,18 +16,13 @@ const createAdmin = async (password) => { // Verify Admin Login const verifyAdmin = async (accessKey, password, token) => { - // 1. Fetch admin from DB const { rows: [admin] } = await db.query( 'SELECT * FROM admins WHERE access_key = $1', [accessKey] ); - - // 2. Verify password if (!admin || !await argon2.verify(admin.argon2_hash, password)) { return false; } - - // 3. Verify TOTP return speakeasy.totp.verify({ secret: admin.totp_secret, encoding: 'base32', diff --git a/server/controllers/adminController.js b/server/controllers/adminController.js index 8b9c0a9..d4a20ae 100644 --- a/server/controllers/adminController.js +++ b/server/controllers/adminController.js @@ -25,13 +25,17 @@ exports.login = async (accessKey, password) => { exports.publishResult = async (data, authorization) => { const token = authorization?.split(' ')[1]; const [admin] = await db.query('SELECT id FROM admins WHERE session_token = ?', [token]); - if (!admin) throw { status: 401, message: 'Unauthorized' }; const { team, date, result } = data; + // validate if the team exists + const teams = await db.query('SELECT id FROM teams WHERE name = ?', [team.toUpperCase()]); + if (!teams.length) throw { status: 400, message: 'Team does not exist. Create team first.' }; + + // publish result using team id await db.query(` INSERT INTO results (team_id, result_date, result) - SELECT id, ?, ? FROM teams WHERE name = ? + VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE result = VALUES(result) - `, [date, result, team.toUpperCase()]); + `, [teams[0].id, date, result]); }; diff --git a/server/controllers/resultController.js b/server/controllers/resultController.js new file mode 100644 index 0000000..24a44a4 --- /dev/null +++ b/server/controllers/resultController.js @@ -0,0 +1,43 @@ +const db = require('../db'); + +exports.getMonthlyResults = async (req, res) => { + try { + const { team, month } = req.body; + if (!team || !month) { + return res.status(400).json({ error: 'Team and month are required.' }); + } + const results = await db.query(` + SELECT r.result, r.result_date + FROM results r + JOIN teams t ON r.team_id = t.id + WHERE t.name = ? AND DATE_FORMAT(r.result_date, '%Y-%m') = ? + `, [team.toUpperCase(), month]); + if (results.length === 0) { + return res.status(404).json({ message: 'No results found for this team in the specified month.' }); + } + res.json(results); + } catch (error) { + res.status(500).json({ error: 'Server error' }); + } +}; + +exports.getDailyResults = async (req, res) => { + try { + const date = req.query.date; + if (!date) { + return res.status(400).json({ error: 'Date query parameter is required.' }); + } + const results = await db.query(` + SELECT t.name as team, r.result, r.result_date + FROM results r + JOIN teams t ON r.team_id = t.id + WHERE r.result_date = ? + `, [date]); + if (results.length === 0) { + return res.status(404).json({ message: 'No results found for the specified date.' }); + } + res.json(results); + } catch (error) { + res.status(500).json({ error: 'Server error' }); + } +}; diff --git a/server/controllers/teamController.js b/server/controllers/teamController.js index 5030e5a..1d16f50 100644 --- a/server/controllers/teamController.js +++ b/server/controllers/teamController.js @@ -1,6 +1,5 @@ const db = require('../db'); -// Get all teams exports.getAllTeams = async (req, res) => { try { const teams = await db.query('SELECT * FROM teams'); @@ -11,7 +10,6 @@ exports.getAllTeams = async (req, res) => { } }; -// Create a new team exports.createTeam = async (req, res) => { try { const { name, announcement_time } = req.body; @@ -32,7 +30,6 @@ exports.createTeam = async (req, res) => { } }; -// Update a team exports.updateTeam = async (req, res) => { try { const { id } = req.params; @@ -66,7 +63,6 @@ exports.updateTeam = async (req, res) => { } }; -// Delete a team exports.deleteTeam = async (req, res) => { try { const { id } = req.params; diff --git a/server/middlewares/security.js b/server/middlewares/security.js index 4da2fef..494d807 100644 --- a/server/middlewares/security.js +++ b/server/middlewares/security.js @@ -1,17 +1,37 @@ const crypto = require('crypto'); +function sanitize(input) { + if (typeof input === 'string') { + return input.replace(//g, ">"); + } + if (Array.isArray(input)) { + return input.map(sanitize); + } + if (input && typeof input === 'object') { + const sanitizedObj = {}; + for (const key in input) { + if (Object.hasOwnProperty.call(input, key)) { + sanitizedObj[key] = sanitize(input[key]); + } + } + return sanitizedObj; + } + return input; +} + module.exports = { - // IP Anonymization anonymizeIP: (req, res, next) => { const ip = req.ip || '127.0.0.1'; - const salt = Math.floor(Date.now() / 3600000); // Hourly salt + const salt = Math.floor(Date.now() / 3600000); req.anonymizedIP = crypto.createHash('sha3-256') .update(ip + salt + process.env.IP_PEPPER) .digest('hex'); next(); }, sanitizeInput: (req, res, next) => { - // ...implement input sanitization if needed... + if (req.body) req.body = sanitize(req.body); + if (req.query) req.query = sanitize(req.query); + if (req.params) req.params = sanitize(req.params); next(); } }; \ No newline at end of file diff --git a/server/postman_collection.json b/server/postman_collection.json index a1fcf95..7731854 100644 --- a/server/postman_collection.json +++ b/server/postman_collection.json @@ -1,7 +1,7 @@ { "info": { "_postman_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", - "name": "Satta Backend API", + "name": "Kings Backend API", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "item": [ @@ -156,6 +156,77 @@ "path": ["api", "health"] } } + }, + { + "name": "Test Sanitization", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"\",\n \"announcement_time\": \"02:30:00\"\n}" + }, + "url": { + "raw": "http://localhost:3000/api/teams", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3000", + "path": [ + "api", + "teams" + ] + } + } + }, + { + "name": "Get Monthly Results", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"team\": \"BIKANER SUPER\",\n \"month\": \"2025-03\"\n}" + }, + "url": { + "raw": "http://localhost:3000/api/results/monthly", + "protocol": "http", + "host": ["localhost"], + "port": "3000", + "path": ["api", "results", "monthly"] + } + } + }, + { + "name": "Get Daily Results", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3000/api/results/daily?date=2025-03-12", + "protocol": "http", + "host": ["localhost"], + "port": "3000", + "path": ["api", "results", "daily"], + "query": [ + { + "key": "date", + "value": "2025-03-12" + } + ] + } + } } ] } diff --git a/server/readme.md b/server/readme.md index 7c42d44..b05a4b1 100644 --- a/server/readme.md +++ b/server/readme.md @@ -1,7 +1,7 @@ -# Satta Backend API +# Kings Backend API ## Overview -This project provides a backend API for the Satta system. It includes endpoints for managing teams, publishing results, admin authentication, and a simple caching mechanism. +Kings Backend API is a RESTful API for managing teams, publishing match/show results, and handling admin authentication. It also includes a simple in-memory caching mechanism, input sanitization, and rate limiting for security. ## Prerequisites - Node.js (v14 or higher) @@ -9,12 +9,13 @@ This project provides a backend API for the Satta system. It includes endpoints ## Installation -1. Clone the repository: +1. **Clone the Repository** ``` git clone cd kingproject/bazar3 ``` -2. Install dependencies: + +2. **Install Dependencies** ``` cd server npm install @@ -22,7 +23,9 @@ This project provides a backend API for the Satta system. It includes endpoints ## Configuration -1. Create a `.env` file in `/server` (or modify the existing one) with the following variables: +1. **Environment Variables** + + Create a `.env` file in the `/server` directory with the following variables: ``` DB_HOST=localhost DB_USER=user @@ -35,11 +38,13 @@ This project provides a backend API for the Satta system. It includes endpoints ## Database Setup -1. Import the schema by running the SQL file `/server/schema.sql` in your MySQL client: +1. **Import Schema** + + Run the following command in your MySQL client to create the database and tables: ``` mysql -u user -p < server/schema.sql ``` -2. Ensure the database `kingdb_prod` is created with the required tables (teams, results, admins). + This creates the `kingdb_prod` database and the required tables: `teams`, `results`, and `admins`. ## Admin Account Setup @@ -47,64 +52,120 @@ To create an admin account, run: ``` npm run create-admin -- ``` -This command will output an `Access Key` which you'll use for admin login. +This script will output an `Access Key` for admin login. ## Running the Server -Start the API server with: +Start the API server by running: ``` npm start ``` -The server listens on the port specified in the `.env` file (default 3000). +The server will listen on the port specified in your `.env` file (default is 3000). ## API Endpoints ### Public Endpoints -- **GET /api/results?team=&date=** - Retrieve the result for a specified team and date. +- **GET /api/results?team=<TEAM_NAME>&date=<YYYY-MM-DD>** + Retrieve the result for a specified team and date. - **GET /api/today** Retrieve all results for the current day. - **GET /api/health** - Basic health check endpoint to verify server and database connectivity. + Health check endpoint to verify server and database connectivity. +- **POST /api/results/monthly** + Get monthly results for a team. + _Request Body Example:_ + ```json + { + "team": "BIKANER SUPER", + "month": "2025-03" + } + ``` +- **GET /api/results/daily?date=<YYYY-MM-DD>** + Get daily results for all teams. ### Admin Endpoints - **POST /admin/login** - Login using `accessKey` and `password` to receive a session token. + Log in using `accessKey` and `password` to receive a session token. + _Request Body Example:_ + ```json + { + "accessKey": "", + "password": "" + } + ``` - **POST /admin/results** - Publish a result. Requires authorization header with the token: - `Authorization: Bearer ` + Publish a result. Requires an authorization header with the session token. + _Request Body Example:_ + ```json + { + "team": "NEW TEAM", + "date": "2025-03-12", + "result": "45" + } + ``` ### Team Endpoints - **GET /api/teams** Retrieve all teams. - **POST /api/teams** Create a new team. Requires `name` and `announcement_time` in the body. + _Request Body Example:_ + ```json + { + "name": "NEW TEAM", + "announcement_time": "02:30:00" + } + ``` - **PUT /api/teams/:id** Update a team. - **DELETE /api/teams/:id** Delete a team. +### Testing Sanitization +A sample endpoint (POST /api/teams) will sanitize HTML input. For example, sending: +```json +{ + "name": "", + "announcement_time": "02:30:00" +} +``` +will have the `<` and `>` characters escaped to protect against XSS. + ## Testing the API -A Postman collection is provided in `/server/postman_collection.json`. You can import this collection into Postman to test all endpoints easily. +1. **Using Postman** -Additionally, a simple test script is available: -``` -npm run test-api -``` -This script uses `axios` to perform a sequence of API calls, including admin login, creating a team, fetching teams, updating, deleting, and publishing a result. + Import the Postman collection from `/server/postman_collection.json` to test all endpoints, including admin authentication, team management, result retrieval, and sanitization. + +2. **Using the Test Script** + + A test script is available that performs a sequence of API calls: + ``` + npm run test-api + ``` + This script uses `axios` to: + - Log in as an admin. + - Create, fetch, update, and delete teams. + - Publish a result. ## Caching -Results are cached in-memory for 5 minutes. Any write operations (POST, PUT, DELETE) clear the cache automatically. + +- Results are cached in memory for 5 minutes. +- Any write operations (POST, PUT, DELETE) clear the cache automatically. ## Rate Limiting and Security -- Rate limiting is implemented to allow 100 requests per minute per anonymized IP. -- IP addresses are anonymized using SHA3-256 with a salt and a secret pepper before being used for rate limiting. +- **Rate Limiting:** + The API allows 100 requests per minute per anonymized IP, using SHA3-256 based IP anonymization. +- **Input Sanitization:** + The middleware sanitizes incoming data (body, query, params) by escaping HTML characters to prevent XSS. +- **SQL Injection Protection:** + SQL queries use prepared statements with parameterized queries, ensuring inputs and queries remain separate. ## Additional Notes -- For input validation, the project leverages Joi. -- Changes to the project configuration or dependency versions may require updating the readme accordingly. +- Input validation is implemented using Joi. +- Keep your environment variables secure. +- Modify configurations as necessary when upgrading dependency versions. ## License Please include your project's license details here. diff --git a/server/routes/public.js b/server/routes/public.js index 245b5b4..244c433 100644 --- a/server/routes/public.js +++ b/server/routes/public.js @@ -2,8 +2,8 @@ const express = require('express'); const router = express.Router(); const db = require('../db'); const cache = require('../cache'); +const resultController = require('../controllers/resultController'); -// Get specific result router.get('/results', async (req, res) => { try { const { team, date } = req.query; @@ -29,7 +29,6 @@ router.get('/results', async (req, res) => { } }); -// Get all today's results router.get('/today', async (req, res) => { try { const today = new Date().toISOString().split('T')[0]; @@ -63,4 +62,10 @@ router.get('/health', async (req, res) => { } }); +// (expects body: { team, month }) +router.post('/results/monthly', resultController.getMonthlyResults); + +// (expects query param: date=YYYY-MM-DD) +router.get('/results/daily', resultController.getDailyResults); + module.exports = router; \ No newline at end of file diff --git a/server/routes/team.js b/server/routes/team.js index 979bf5d..15871b2 100644 --- a/server/routes/team.js +++ b/server/routes/team.js @@ -3,16 +3,12 @@ const router = express.Router(); const teamController = require('../controllers/teamController'); const { validateTeam } = require('../middlewares/validation'); -// Get all teams router.get('/', teamController.getAllTeams); -// Create a new team router.post('/', validateTeam, teamController.createTeam); -// Update a team router.put('/:id', validateTeam, teamController.updateTeam); -// Delete a team router.delete('/:id', teamController.deleteTeam); module.exports = router; diff --git a/server/scripts/test-api.js b/server/scripts/test-api.js index 34f866b..157a76f 100644 --- a/server/scripts/test-api.js +++ b/server/scripts/test-api.js @@ -13,7 +13,6 @@ const BASE_URL = 'http://localhost:3000'; const sessionToken = loginResponse.data.token; console.log('Login successful. Session Token:', sessionToken); - // Create a Team console.log('Creating a new team...'); const createTeamResponse = await axios.post( `${BASE_URL}/api/teams`, @@ -32,7 +31,6 @@ const BASE_URL = 'http://localhost:3000'; const teamsResponse = await axios.get(`${BASE_URL}/api/teams`); console.log('Teams:', teamsResponse.data); - // Update a Team console.log('Updating a team...'); const updateTeamResponse = await axios.put( `${BASE_URL}/api/teams/1`, @@ -46,14 +44,12 @@ const BASE_URL = 'http://localhost:3000'; ); console.log('Team updated:', updateTeamResponse.data); - // Delete a Team console.log('Deleting a team...'); const deleteTeamResponse = await axios.delete(`${BASE_URL}/api/teams/1`, { headers: { Authorization: `Bearer ${sessionToken}` } }); console.log('Team deleted:', deleteTeamResponse.data); - // Publish a Result console.log('Publishing a result...'); const publishResultResponse = await axios.post( `${BASE_URL}/admin/results`,