From 9cc08debeda8e529b93b9eaa2200c9c8ecff9555 Mon Sep 17 00:00:00 2001 From: naveenk Date: Wed, 19 Mar 2025 17:06:30 +0530 Subject: [PATCH] major changes --- server/cache.js | 12 ++ server/controllers/adminController.js | 111 -------------- server/controllers/resultController.js | 50 ------- server/middlewares/authorization.js | 19 +++ server/middlewares/validation.js | 60 ++++++-- server/postman_collection.json | 83 +++++------ server/readme.md | 55 ++++--- server/routes/admin.js | 33 ++--- server/routes/public.js | 103 ++++++------- server/routes/team.js | 4 +- server/schema.sql | 17 ++- server/scripts/test-api.js | 3 +- server/services/adminService.js | 95 ++++++++++++ server/services/resultService.js | 135 ++++++++++++++++++ .../teamService.js} | 6 + 15 files changed, 454 insertions(+), 332 deletions(-) delete mode 100644 server/controllers/adminController.js delete mode 100644 server/controllers/resultController.js create mode 100644 server/middlewares/authorization.js create mode 100644 server/services/adminService.js create mode 100644 server/services/resultService.js rename server/{controllers/teamController.js => services/teamService.js} (57%) diff --git a/server/cache.js b/server/cache.js index 101a32e..1c2957a 100644 --- a/server/cache.js +++ b/server/cache.js @@ -2,6 +2,7 @@ class SattaCache { constructor() { this.store = new Map(); this.ttl = 300000; + this.startCleanup(); } set(key, value) { @@ -26,6 +27,17 @@ class SattaCache { clear() { this.store.clear(); } + + startCleanup() { + setInterval(() => { + const now = Date.now(); + for (const [key, value] of this.store.entries()) { + if (value.expires < now) { + this.store.delete(key); + } + } + }, 60000); // Run every 60 seconds + } } module.exports = new SattaCache(); \ No newline at end of file diff --git a/server/controllers/adminController.js b/server/controllers/adminController.js deleted file mode 100644 index 7027453..0000000 --- a/server/controllers/adminController.js +++ /dev/null @@ -1,111 +0,0 @@ -const db = require('../db'); -const crypto = require('crypto'); -const argon2 = require('argon2'); - -exports.login = async (accessKey, password) => { - const [admin] = await db.query( - 'SELECT * FROM admins WHERE access_key = ? AND is_active = 1', - [accessKey] - ); - - if (!admin) throw { status: 401, message: 'Invalid credentials' }; - - const validPass = await argon2.verify(admin.argon2_hash, password); - if (!validPass) throw { status: 401, message: 'Invalid password' }; - - const sessionToken = crypto.randomBytes(32).toString('hex'); - await db.query( - 'UPDATE admins SET session_token = ?, last_access = NOW() WHERE id = ?', - [sessionToken, admin.id] - ); - - return sessionToken; -}; - -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, announcement_time } = data; // renamed draw_time - // 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, announcement_time) - VALUES (?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - result = VALUES(result), - announcement_time = VALUES(announcement_time) - `, [teams[0].id, date, result, announcement_time]); -}; - -exports.getResultsByTeam = async (teamName, 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' }; - if (!teamName) throw { status: 400, message: 'Team name is required' }; - - return db.query(` - SELECT r.*, t.name AS team_name - FROM results r - JOIN teams t ON r.team_id = t.id - WHERE t.name = ? - `, [teamName.toUpperCase()]); -}; - -exports.createTeam = 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 { name } = data; - if (!name) throw { status: 400, message: 'Name is required' }; - await db.query('INSERT INTO teams (name) VALUES (?)', [name.toUpperCase()]); - return { success: true, message: 'Team created successfully' }; -}; - -exports.updateTeam = async (id, 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 { name } = data; - if (!name) throw { status: 400, message: 'Name is required' }; - await db.query('UPDATE teams SET name = ? WHERE id = ?', [name.toUpperCase(), id]); - return { success: true, message: 'Team updated successfully' }; -}; - -exports.deleteTeam = async (id, 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' }; - - await db.query('DELETE FROM teams WHERE id = ?', [id]); - return { success: true, message: 'Team deleted successfully' }; -}; - -exports.updateResultById = async (id, 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, announcement_time } = data; - const teams = await db.query('SELECT id FROM teams WHERE name = ?', [team.toUpperCase()]); - if (!teams.length) throw { status: 400, message: 'Team does not exist' }; - - await db.query( - 'UPDATE results SET team_id = ?, result_date = ?, result = ?, announcement_time = ? WHERE id = ?', - [teams[0].id, date, result, announcement_time, id] - ); -}; - -exports.deleteResultById = async (id, 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' }; - - await db.query('DELETE FROM results WHERE id = ?', [id]); -}; diff --git a/server/controllers/resultController.js b/server/controllers/resultController.js deleted file mode 100644 index 21a5c0c..0000000 --- a/server/controllers/resultController.js +++ /dev/null @@ -1,50 +0,0 @@ -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.' }); - } - if (!/^\d{4}-\d{2}$/.test(month)) { - return res.status(400).json({ error: 'Invalid month format. Use YYYY-MM.' }); - } - const results = await db.query(` - SELECT r.result, r.result_date, r.announcement_time - FROM results r - JOIN teams t ON r.team_id = t.id - WHERE t.name = ? AND DATE_FORMAT(r.result_date, '%Y-%m') = ? - AND (r.result_date < CURDATE() - OR (r.result_date = CURDATE() AND r.announcement_time <= CURTIME())) - `, [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, r.announcement_time - FROM results r - JOIN teams t ON r.team_id = t.id - WHERE r.result_date = ? - AND (r.result_date < CURDATE() - OR (r.result_date = CURDATE() AND r.announcement_time <= CURTIME())) - `, [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/middlewares/authorization.js b/server/middlewares/authorization.js new file mode 100644 index 0000000..6f24770 --- /dev/null +++ b/server/middlewares/authorization.js @@ -0,0 +1,19 @@ +const db = require('../db'); + +exports.authorizeAdmin = async (req, res, next) => { + try { + const token = req.headers.authorization?.split(' ')[1]; + if (!token) { + return res.status(401).json({ error: 'Authorization token is required.' }); + } + + const [admin] = await db.query('SELECT id FROM admins WHERE session_token = ?', [token]); + if (!admin) { + return res.status(401).json({ error: 'Unauthorized access.' }); + } + + next(); + } catch (error) { + res.status(500).json({ error: 'Server error during authorization.' }); + } +}; diff --git a/server/middlewares/validation.js b/server/middlewares/validation.js index e19088d..508a537 100644 --- a/server/middlewares/validation.js +++ b/server/middlewares/validation.js @@ -31,13 +31,6 @@ exports.validateResult = (req, res, next) => { 'string.pattern.base': 'Team name must only contain alphanumeric characters and spaces.', 'string.max': 'Team name must not exceed 100 characters.' }), - date: Joi.string() - .pattern(/^\d{4}-\d{2}-\d{2}$/) - .required() - .messages({ - 'string.empty': 'Date is required.', - 'string.pattern.base': 'Date must be in YYYY-MM-DD format.' - }), result: Joi.string() .pattern(/^[0-9]+$/) .max(10) @@ -47,12 +40,57 @@ exports.validateResult = (req, res, next) => { 'string.pattern.base': 'Result must only contain numeric characters.', 'string.max': 'Result must not exceed 10 characters.' }), - announcement_time: Joi.string() - .pattern(/^\d{2}:\d{2}:\d{2}$/) + result_time: Joi.string() + .pattern(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/) .required() .messages({ - 'string.empty': 'Announcement time is required.', - 'string.pattern.base': 'Announcement time must be in HH:MM:SS format.' + 'string.empty': 'Result time is required.', + 'string.pattern.base': 'Result time must be in YYYY-MM-DD HH:MM:SS format.' + }) + }); + + const { error } = schema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } + next(); +}; + +exports.validateDate = (req, res, next) => { + const schema = Joi.object({ + date: Joi.string() + .pattern(/^\d{4}-\d{2}-\d{2}$/) + .required() + .messages({ + 'string.empty': 'Date is required.', + 'string.pattern.base': 'Date must be in YYYY-MM-DD format.' + }) + }); + + const { error } = schema.validate(req.query); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } + next(); +}; + +exports.validateMonthlyResults = (req, res, next) => { + const schema = Joi.object({ + team: Joi.string() + .pattern(/^[a-zA-Z0-9\s]+$/) + .max(100) + .required() + .messages({ + 'string.empty': 'Team name is required.', + 'string.pattern.base': 'Team name must only contain alphanumeric characters and spaces.', + 'string.max': 'Team name must not exceed 100 characters.' + }), + month: Joi.string() + .pattern(/^\d{4}-\d{2}$/) + .required() + .messages({ + 'string.empty': 'Month is required.', + 'string.pattern.base': 'Month must be in YYYY-MM format.' }) }); diff --git a/server/postman_collection.json b/server/postman_collection.json index a75b8ea..fc9be50 100644 --- a/server/postman_collection.json +++ b/server/postman_collection.json @@ -20,10 +20,10 @@ "raw": "{\n \"accessKey\": \"\",\n \"password\": \"\"\n}" }, "url": { - "raw": "http://localhost:3000/admin/login", + "raw": "http://localhost:5500/admin/login", "protocol": "http", "host": ["localhost"], - "port": "3000", + "port": "5500", "path": ["admin", "login"] } } @@ -44,13 +44,13 @@ ], "body": { "mode": "raw", - "raw": "{\n \"team\": \"BIKANER SUPER\",\n \"date\": \"2025-03-12\",\n \"result\": \"45\",\n \"announcement_time\": \"02:30:00\"\n}" + "raw": "{\n \"team\": \"BIKANER SUPER\",\n \"result\": \"45\",\n \"result_time\": \"2025-03-12 15:00:00\"\n}" }, "url": { - "raw": "http://localhost:3000/admin/results", + "raw": "http://localhost:5500/admin/results", "protocol": "http", "host": ["localhost"], - "port": "3000", + "port": "5500", "path": ["admin", "results"] } } @@ -61,10 +61,10 @@ "method": "GET", "header": [], "url": { - "raw": "http://localhost:3000/api/teams", + "raw": "http://localhost:5500/api/teams", "protocol": "http", "host": ["localhost"], - "port": "3000", + "port": "5500", "path": ["api", "teams"] } } @@ -85,13 +85,13 @@ ], "body": { "mode": "raw", - "raw": "{\n \"team\": \"UPDATED TEAM\",\n \"date\": \"2025-03-12\",\n \"result\": \"55\",\n \"announcement_time\": \"03:00:00\"\n}" + "raw": "{\n \"team\": \"UPDATED TEAM\",\n \"result\": \"55\",\n \"result_time\": \"2025-03-12 16:00:00\"\n}" }, "url": { - "raw": "http://localhost:3000/admin/results/1", + "raw": "http://localhost:5500/admin/results/1", "protocol": "http", "host": ["localhost"], - "port": "3000", + "port": "5500", "path": ["admin", "results", "1"] } } @@ -107,10 +107,10 @@ } ], "url": { - "raw": "http://localhost:3000/admin/results/1", + "raw": "http://localhost:5500/admin/results/1", "protocol": "http", "host": ["localhost"], - "port": "3000", + "port": "5500", "path": ["admin", "results", "1"] } } @@ -126,10 +126,10 @@ } ], "url": { - "raw": "http://localhost:3000/admin/results?team=BIKANER SUPER", + "raw": "http://localhost:5500/admin/results?team=BIKANER SUPER", "protocol": "http", "host": ["localhost"], - "port": "3000", + "port": "5500", "path": ["admin", "results"], "query": [ { @@ -159,10 +159,10 @@ "raw": "{\n \"name\": \"NEW TEAM\"\n}" }, "url": { - "raw": "http://localhost:3000/admin/teams", + "raw": "http://localhost:5500/admin/teams", "protocol": "http", "host": ["localhost"], - "port": "3000", + "port": "5500", "path": ["admin", "teams"] } } @@ -186,10 +186,10 @@ "raw": "{\n \"name\": \"UPDATED TEAM\"\n}" }, "url": { - "raw": "http://localhost:3000/admin/teams/1", + "raw": "http://localhost:5500/admin/teams/1", "protocol": "http", "host": ["localhost"], - "port": "3000", + "port": "5500", "path": ["admin", "teams", "1"] } } @@ -205,10 +205,10 @@ } ], "url": { - "raw": "http://localhost:3000/admin/teams/1", + "raw": "http://localhost:5500/admin/teams/1", "protocol": "http", "host": ["localhost"], - "port": "3000", + "port": "5500", "path": ["admin", "teams", "1"] } } @@ -219,10 +219,10 @@ "method": "GET", "header": [], "url": { - "raw": "http://localhost:3000/api/today", + "raw": "http://localhost:5500/api/today", "protocol": "http", "host": ["localhost"], - "port": "3000", + "port": "5500", "path": ["api", "today"] } } @@ -242,10 +242,10 @@ "raw": "{\n \"team\": \"BIKANER SUPER\",\n \"month\": \"2025-03\"\n}" }, "url": { - "raw": "http://localhost:3000/api/results/monthly", + "raw": "http://localhost:5500/api/results/monthly", "protocol": "http", "host": ["localhost"], - "port": "3000", + "port": "5500", "path": ["api", "results", "monthly"] } } @@ -256,10 +256,10 @@ "method": "GET", "header": [], "url": { - "raw": "http://localhost:3000/api/results/daily?date=2025-03-12", + "raw": "http://localhost:5500/api/results/daily?date=2025-03-12", "protocol": "http", "host": ["localhost"], - "port": "3000", + "port": "5500", "path": ["api", "results", "daily"], "query": [ { @@ -271,29 +271,22 @@ } }, { - "name": "Publish Result Validation Example", + "name": "Get Results By Team (Public)", "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer " - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"team\": \"INVALID@TEAM\",\n \"date\": \"2025-03-12\",\n \"result\": \"INVALID_RESULT\",\n \"announcement_time\": \"INVALID_TIME\"\n}" - }, + "method": "GET", + "header": [], "url": { - "raw": "http://localhost:3000/admin/results", + "raw": "http://localhost:5500/api/results/team?team=BIKANER SUPER", "protocol": "http", "host": ["localhost"], - "port": "3000", - "path": ["admin", "results"] + "port": "5500", + "path": ["api", "results", "team"], + "query": [ + { + "key": "team", + "value": "BIKANER SUPER" + } + ] } } } diff --git a/server/readme.md b/server/readme.md index bc341c8..1c81f19 100644 --- a/server/readme.md +++ b/server/readme.md @@ -10,14 +10,13 @@ Kings Backend API is a RESTful API for managing teams, publishing match/show res ## Installation 1. **Clone the Repository** - ``` + ```bash git clone - cd kingproject/bazar3 + cd bazar3/server ``` 2. **Install Dependencies** - ``` - cd server + ```bash npm install ``` @@ -26,14 +25,14 @@ Kings Backend API is a RESTful API for managing teams, publishing match/show res 1. **Environment Variables** Create a `.env` file in the `/server` directory with the following variables: - ``` + ```env DB_HOST=localhost DB_USER=user DB_PASS=password DB_NAME=kingdb_prod IP_PEPPER=your_ip_pepper JWT_SECRET= - PORT=3000 + PORT=5500 ``` ## Database Setup @@ -41,7 +40,7 @@ Kings Backend API is a RESTful API for managing teams, publishing match/show res 1. **Import Schema** Run the following command in your MySQL client to create the database and tables: - ``` + ```bash mysql -u user -p < server/schema.sql ``` This creates the `kingdb_prod` database and the required tables: `teams`, `results`, and `admins`. @@ -49,7 +48,7 @@ Kings Backend API is a RESTful API for managing teams, publishing match/show res ## Admin Account Setup To create an admin account, run: -``` +```bash npm run create-admin -- ``` This script will output an `Access Key` for admin login. @@ -57,15 +56,15 @@ This script will output an `Access Key` for admin login. ## Running the Server Start the API server by running: -``` +```bash npm start ``` -The server will listen on the port specified in your `.env` file (default is 3000). +The server will listen on the port specified in your `.env` file (default is 5500). ## API Endpoints ### Public Endpoints -- **GET /api/results?team=<TEAM_NAME>&date=<YYYY-MM-DD>** +- **GET /api/results?team=&date=** Retrieve the result for a specified team and date. - **GET /api/today** Retrieve all results for the current day. @@ -80,7 +79,7 @@ The server will listen on the port specified in your `.env` file (default is 300 "month": "2025-03" } ``` -- **GET /api/results/daily?date=<YYYY-MM-DD>** +- **GET /api/results/daily?date=** Get daily results for all teams. ### Admin Endpoints @@ -99,8 +98,8 @@ The server will listen on the port specified in your `.env` file (default is 300 ```json { "team": "NEW TEAM", - "date": "2025-03-12", - "result": "45" + "result": "45", + "result_time": "2025-03-12 15:00:00" } ``` @@ -108,30 +107,28 @@ The server will listen on the port specified in your `.env` file (default is 300 - **GET /api/teams** Retrieve all teams (public). -- **POST /api/teams** +- **POST /admin/teams** Create a new team (admin only). + _Request Body Example:_ ```json { "name": "NEW TEAM" } ``` -- **PUT /api/teams/:id** - Update a team (admin only, requires Bearer token). +- **PUT /admin/teams/:id** + Update a team (admin only, requires Bearer token). + _Request Body Example:_ + ```json + { + "name": "UPDATED TEAM" + } + ``` -- **DELETE /api/teams/:id** +- **DELETE /admin/teams/:id** Delete a team (admin only, requires Bearer token). -### Testing Sanitization -A sample endpoint (POST /api/teams) will sanitize HTML input. For example, sending: -```json -{ - "name": "" -} -``` -will have the `<` and `>` characters escaped to protect against XSS. - -## Testing the API +### Testing the API 1. **Using Postman** @@ -140,7 +137,7 @@ will have the `<` and `>` characters escaped to protect against XSS. 2. **Using the Test Script** A test script is available that performs a sequence of API calls: - ``` + ```bash npm run test-api ``` This script uses `axios` to: diff --git a/server/routes/admin.js b/server/routes/admin.js index 771eae7..65bcce4 100644 --- a/server/routes/admin.js +++ b/server/routes/admin.js @@ -1,75 +1,76 @@ const express = require('express'); const router = express.Router(); -const adminController = require('../controllers/adminController'); +const adminService = require('../services/adminService'); const { validateResult } = require('../middlewares/validation'); +const { authorizeAdmin } = require('../middlewares/authorization'); router.post('/login', async (req, res, next) => { try { const { accessKey, password } = req.body; - const token = await adminController.login(accessKey, password); + const token = await adminService.login(accessKey, password); res.json({ token }); } catch (error) { next(error); } }); -router.post('/results', validateResult, async (req, res, next) => { +router.post('/results', authorizeAdmin, validateResult, async (req, res, next) => { try { - await adminController.publishResult(req.body, req.headers.authorization); + await adminService.publishResult(req.body); res.json({ success: true }); } catch (error) { next(error); } }); -router.put('/results/:id', validateResult, async (req, res, next) => { +router.put('/results/:id', authorizeAdmin, validateResult, async (req, res, next) => { try { - await adminController.updateResultById(req.params.id, req.body, req.headers.authorization); + await adminService.updateResultById(req.params.id, req.body); res.json({ success: true }); } catch (error) { next(error); } }); -router.delete('/results/:id', async (req, res, next) => { +router.delete('/results/:id', authorizeAdmin, async (req, res, next) => { try { - await adminController.deleteResultById(req.params.id, req.headers.authorization); + await adminService.deleteResultById(req.params.id); res.json({ success: true }); } catch (error) { next(error); } }); -router.get('/results', async (req, res, next) => { +router.get('/results', authorizeAdmin, async (req, res, next) => { try { - const data = await adminController.getResultsByTeam(req.query.team, req.headers.authorization); + const data = await adminService.getResultsByTeam(req.query.team); res.json(data); } catch (error) { next(error); } }); -router.post('/teams', async (req, res, next) => { +router.post('/teams', authorizeAdmin, async (req, res, next) => { try { - const result = await adminController.createTeam(req.body, req.headers.authorization); + const result = await adminService.createTeam(req.body); res.json(result); } catch (error) { next(error); } }); -router.put('/teams/:id', async (req, res, next) => { +router.put('/teams/:id', authorizeAdmin, async (req, res, next) => { try { - const result = await adminController.updateTeam(req.params.id, req.body, req.headers.authorization); + const result = await adminService.updateTeam(req.params.id, req.body); res.json(result); } catch (error) { next(error); } }); -router.delete('/teams/:id', async (req, res, next) => { +router.delete('/teams/:id', authorizeAdmin, async (req, res, next) => { try { - const result = await adminController.deleteTeam(req.params.id, req.headers.authorization); + const result = await adminService.deleteTeam(req.params.id); res.json(result); } catch (error) { next(error); diff --git a/server/routes/public.js b/server/routes/public.js index 9cfe1f8..9871946 100644 --- a/server/routes/public.js +++ b/server/routes/public.js @@ -1,75 +1,64 @@ const express = require('express'); const router = express.Router(); -const db = require('../db'); -const cache = require('../cache'); -const resultController = require('../controllers/resultController'); +const resultService = require('../services/resultService'); +const { validateDate, validateMonthlyResults } = require('../middlewares/validation'); -router.get('/results', async (req, res) => { +router.get('/results', async (req, res, next) => { try { const { team, date } = req.query; - const cacheKey = `${team}:${date}`; - - if (cache.has(cacheKey)) { - return res.json(cache.get(cacheKey)); - } - - const [result] = await db.query(` - SELECT r.result, r.result_date, r.announcement_time, t.name AS team - FROM results r - JOIN teams t ON r.team_id = t.id - WHERE t.name = ? AND r.result_date = ? - AND (r.result_date < CURDATE() - OR (r.result_date = CURDATE() AND r.announcement_time <= CURTIME())) - `, [team.toUpperCase(), date]); - - if (!result) return res.status(404).json({ error: 'Result not found' }); - - cache.set(cacheKey, result); - res.json(result); - } catch (error) { - res.status(500).json({ error: 'Server error' }); - } -}); - -router.get('/today', async (req, res) => { - try { - const today = new Date().toISOString().split('T')[0]; - const cacheKey = `today:${today}`; - - if (cache.has(cacheKey)) { - return res.json(cache.get(cacheKey)); - } - - const results = await db.query(` - SELECT t.name AS team, r.result, r.result_date, r.announcement_time - FROM results r - JOIN teams t ON r.team_id = t.id - WHERE r.result_date = ? - AND (r.result_date < CURDATE() - OR (r.result_date = CURDATE() AND r.announcement_time <= CURTIME())) - `, [today]); - - cache.set(cacheKey, results); + const results = await resultService.getResultsByTeamAndDate(team, date); res.json(results); } catch (error) { - res.status(500).json({ error: 'Server error' }); + next(error); } }); -// health check -router.get('/health', async (req, res) => { +router.get('/today', async (req, res, next) => { try { - await db.query('SELECT 1'); // Simple query to check DB connection - res.json({ status: 'healthy' }); + const results = await resultService.getTodayResults(); + res.json(results); } catch (error) { - res.status(500).json({ status: 'unhealthy', error: error.message }); + next(error); } }); -// (expects body: { team, month }) -router.post('/results/monthly', resultController.getMonthlyResults); +router.get('/health', async (req, res, next) => { + try { + const healthStatus = await resultService.checkHealth(); + res.json(healthStatus); + } catch (error) { + next(error); + } +}); -// (expects query param: date=YYYY-MM-DD) -router.get('/results/daily', resultController.getDailyResults); +router.post('/results/monthly', validateMonthlyResults, async (req, res, next) => { + try { + const { team, month } = req.body; + const results = await resultService.getMonthlyResults(team, month); + res.json(results); + } catch (error) { + next(error); + } +}); + +router.get('/results/daily', validateDate, async (req, res, next) => { + try { + const { date } = req.query; + const results = await resultService.getDailyResults(date); + res.json(results); + } catch (error) { + next(error); + } +}); + +router.get('/results/team', async (req, res, next) => { + try { + const { team } = req.query; + const results = await resultService.getResultsByTeam(team); + res.json(results); + } catch (error) { + next(error); + } +}); module.exports = router; \ No newline at end of file diff --git a/server/routes/team.js b/server/routes/team.js index 157d450..32dca2b 100644 --- a/server/routes/team.js +++ b/server/routes/team.js @@ -1,7 +1,7 @@ const express = require('express'); const router = express.Router(); -const teamController = require('../controllers/teamController'); +const teamService = require('../services/teamService'); -router.get('/', teamController.getAllTeams); +router.get('/', teamService.getAllTeams); module.exports = router; diff --git a/server/schema.sql b/server/schema.sql index 8ba1b64..6bbad35 100644 --- a/server/schema.sql +++ b/server/schema.sql @@ -5,17 +5,16 @@ CREATE TABLE teams ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL UNIQUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); +) ENGINE=InnoDB; CREATE TABLE results ( id INT AUTO_INCREMENT PRIMARY KEY, team_id INT NOT NULL, - result_date DATE NOT NULL, - announcement_time TIME NOT NULL, - result VARCHAR(10) NOT NULL, - FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE, - UNIQUE KEY uniq_team_date (team_id, result_date) -); + result_time DATETIME NOT NULL, -- When the result will be shown + result VARCHAR(10) NOT NULL DEFAULT '-1', -- Default result if admin hasn't set it + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE +) ENGINE=InnoDB; CREATE TABLE admins ( id INT AUTO_INCREMENT PRIMARY KEY, @@ -23,5 +22,5 @@ CREATE TABLE admins ( argon2_hash TEXT NOT NULL, session_token CHAR(64), is_active BOOLEAN DEFAULT TRUE, - last_access TIMESTAMP -); \ No newline at end of file + last_access TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB; \ No newline at end of file diff --git a/server/scripts/test-api.js b/server/scripts/test-api.js index 2d162d8..099481e 100644 --- a/server/scripts/test-api.js +++ b/server/scripts/test-api.js @@ -54,8 +54,7 @@ const BASE_URL = 'http://localhost:3000'; { team: 'NEW TEAM', date: '2025-03-12', - result: '45', - announcement_time: '02:30:00' + result: '45' }, { headers: { Authorization: `Bearer ${sessionToken}` } diff --git a/server/services/adminService.js b/server/services/adminService.js new file mode 100644 index 0000000..4334795 --- /dev/null +++ b/server/services/adminService.js @@ -0,0 +1,95 @@ +const db = require('../db'); +const crypto = require('crypto'); +const argon2 = require('argon2'); + +exports.login = async (accessKey, password) => { + console.log(`Admin login attempt with accessKey: ${accessKey}`); + try { + const [admin] = await db.query( + 'SELECT * FROM admins WHERE access_key = ? AND is_active = 1', + [accessKey] + ); + + if (!admin) { + console.warn('Invalid accessKey.'); + throw { status: 401, message: 'Invalid credentials' }; + } + + const validPass = await argon2.verify(admin.argon2_hash, password); + if (!validPass) { + console.warn('Invalid password.'); + throw { status: 401, message: 'Invalid password' }; + } + + const sessionToken = crypto.randomBytes(32).toString('hex'); + await db.query( + 'UPDATE admins SET session_token = ?, last_access = NOW() WHERE id = ?', + [sessionToken, admin.id] + ); + + console.log('Admin login successful.'); + return sessionToken; + } catch (error) { + console.error('Error during admin login:', error); + throw error; + } +}; + +exports.publishResult = async (data) => { + const { team, result, result_time } = data; + 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.' }; + + await db.query(` + INSERT INTO results (team_id, result, result_time) + VALUES (?, ?, ?) + ON DUPLICATE KEY UPDATE + result = VALUES(result), + result_time = VALUES(result_time) + `, [teams[0].id, result, result_time]); +}; + +exports.getResultsByTeam = async (teamName) => { + if (!teamName) throw { status: 400, message: 'Team name is required' }; + + return db.query(` + SELECT r.*, t.name AS team_name + FROM results r + JOIN teams t ON r.team_id = t.id + WHERE t.name = ? + `, [teamName.toUpperCase()]); +}; + +exports.createTeam = async (data) => { + const { name } = data; + if (!name) throw { status: 400, message: 'Name is required' }; + await db.query('INSERT INTO teams (name) VALUES (?)', [name.toUpperCase()]); + return { success: true, message: 'Team created successfully' }; +}; + +exports.updateTeam = async (id, data) => { + const { name } = data; + if (!name) throw { status: 400, message: 'Name is required' }; + await db.query('UPDATE teams SET name = ? WHERE id = ?', [name.toUpperCase(), id]); + return { success: true, message: 'Team updated successfully' }; +}; + +exports.deleteTeam = async (id) => { + await db.query('DELETE FROM teams WHERE id = ?', [id]); + return { success: true, message: 'Team deleted successfully' }; +}; + +exports.updateResultById = async (id, data) => { + const { team, result, result_time } = data; + const teams = await db.query('SELECT id FROM teams WHERE name = ?', [team.toUpperCase()]); + if (!teams.length) throw { status: 400, message: 'Team does not exist' }; + + await db.query( + 'UPDATE results SET team_id = ?, result = ?, result_time = ? WHERE id = ?', + [teams[0].id, result, result_time, id] + ); +}; + +exports.deleteResultById = async (id) => { + await db.query('DELETE FROM results WHERE id = ?', [id]); +}; diff --git a/server/services/resultService.js b/server/services/resultService.js new file mode 100644 index 0000000..cfa43b3 --- /dev/null +++ b/server/services/resultService.js @@ -0,0 +1,135 @@ +const db = require('../db'); +const cache = require('../cache'); + +exports.getResultsByTeamAndDate = async (team, date) => { + console.log(`Fetching results for team: ${team}, date: ${date}`); + try { + const cacheKey = `${team}:${date}`; + if (cache.has(cacheKey)) { + console.log('Cache hit for results.'); + return cache.get(cacheKey); + } + + const results = await db.query(` + SELECT r.result_time, + CASE + WHEN NOW() < r.result_time THEN '-1' + ELSE r.result + END AS visible_result, + t.name AS team + FROM results r + JOIN teams t ON r.team_id = t.id + WHERE t.name = ? AND DATE(r.result_time) = ? + `, [team.toUpperCase(), date]); + + if (!results.length) { + console.warn('No results found.'); + return []; + } + + cache.set(cacheKey, results); + return results; + } catch (error) { + console.error('Error fetching results:', error); + throw { status: 500, message: 'Internal Server Error' }; + } +}; + +exports.getTodayResults = async () => { + const today = new Date().toISOString().split('T')[0]; + const cacheKey = `today:${today}`; + + if (cache.has(cacheKey)) { + return cache.get(cacheKey); + } + + const results = await db.query(` + SELECT t.name AS team, r.result_time, + CASE + WHEN NOW() < r.result_time THEN '-1' + ELSE r.result + END AS visible_result + FROM results r + JOIN teams t ON r.team_id = t.id + WHERE DATE(r.result_time) = ? + `, [today]); + + cache.set(cacheKey, results); + return results; +}; + +exports.checkHealth = async () => { + try { + await db.query('SELECT 1'); // Simple query to check DB connection + return { status: 'healthy' }; + } catch (error) { + return { status: 'unhealthy', error: error.message }; + } +}; + +exports.getMonthlyResults = async (team, month) => { + if (!team || !month) { + throw { status: 400, message: 'Team and month are required.' }; + } + if (!/^\d{4}-\d{2}$/.test(month)) { + throw { status: 400, message: 'Invalid month format. Use YYYY-MM.' }; + } + + const results = await db.query(` + SELECT r.result, r.result_time, + CASE + WHEN NOW() < r.result_time THEN '-1' + ELSE r.result + END AS visible_result + FROM results r + JOIN teams t ON r.team_id = t.id + WHERE t.name = ? AND DATE_FORMAT(r.result_time, '%Y-%m') = ? + `, [team.toUpperCase(), month]); + + return results; +}; + +exports.getDailyResults = async (date) => { + if (!date) { + throw { status: 400, message: 'Date is required.' }; + } + + const results = await db.query(` + SELECT t.name AS team, r.result_time, + CASE + WHEN NOW() < r.result_time THEN '-1' + ELSE r.result + END AS visible_result + FROM results r + JOIN teams t ON r.team_id = t.id + WHERE DATE(r.result_time) = ? + `, [date]); + + return results; +}; + +exports.getResultsByTeam = async (team) => { + if (!team) { + throw { status: 400, message: 'Team query parameter is required.' }; + } + + const cacheKey = `team:${team}`; + if (cache.has(cacheKey)) { + return cache.get(cacheKey); + } + + const results = await db.query(` + SELECT r.result_time, + CASE + WHEN NOW() < r.result_time THEN '-1' + ELSE r.result + END AS visible_result, + t.name AS team + FROM results r + JOIN teams t ON r.team_id = t.id + WHERE t.name = ? + `, [team.toUpperCase()]); + + cache.set(cacheKey, results); + return results; +}; diff --git a/server/controllers/teamController.js b/server/services/teamService.js similarity index 57% rename from server/controllers/teamController.js rename to server/services/teamService.js index 58ad971..6e465e1 100644 --- a/server/controllers/teamController.js +++ b/server/services/teamService.js @@ -1,8 +1,14 @@ const db = require('../db'); exports.getAllTeams = async (req, res) => { + console.log('Fetching all teams...'); try { const teams = await db.query('SELECT * FROM teams'); + if (!teams.length) { + console.log('No teams found.'); + return res.status(404).json({ error: 'No teams found.' }); + } + console.log(`Fetched ${teams.length} teams.`); res.json(teams); } catch (error) { console.error('Error fetching teams:', error);