This commit is contained in:
shivam 2025-03-19 20:52:21 +05:30
commit 5508a5af0b
16 changed files with 467 additions and 334 deletions

View File

@ -2,6 +2,7 @@ class SattaCache {
constructor() { constructor() {
this.store = new Map(); this.store = new Map();
this.ttl = 300000; this.ttl = 300000;
this.startCleanup();
} }
set(key, value) { set(key, value) {
@ -26,6 +27,17 @@ class SattaCache {
clear() { clear() {
this.store.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(); module.exports = new SattaCache();

View File

@ -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]);
};

View File

@ -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' });
}
};

View File

@ -1,5 +1,11 @@
const mysql = require('mysql2/promise'); const mysql = require('mysql2/promise');
// Validate environment variables
if (!process.env.DB_HOST || !process.env.DB_USER || !process.env.DB_PASS || !process.env.DB_NAME) {
console.error('Database configuration is missing in .env file.');
process.exit(1);
}
const pool = mysql.createPool({ const pool = mysql.createPool({
host: process.env.DB_HOST, host: process.env.DB_HOST,
user: process.env.DB_USER, user: process.env.DB_USER,
@ -13,7 +19,12 @@ const pool = mysql.createPool({
module.exports = { module.exports = {
query: async (sql, params) => { query: async (sql, params) => {
const [rows] = await pool.execute(sql, params); try {
return rows; const [rows] = await pool.execute(sql, params);
return rows;
} catch (error) {
console.error('Database query error:', error.message);
throw error;
}
} }
}; };

View File

@ -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.' });
}
};

View File

@ -31,13 +31,6 @@ exports.validateResult = (req, res, next) => {
'string.pattern.base': 'Team name must only contain alphanumeric characters and spaces.', 'string.pattern.base': 'Team name must only contain alphanumeric characters and spaces.',
'string.max': 'Team name must not exceed 100 characters.' '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() result: Joi.string()
.pattern(/^[0-9]+$/) .pattern(/^[0-9]+$/)
.max(10) .max(10)
@ -47,12 +40,57 @@ exports.validateResult = (req, res, next) => {
'string.pattern.base': 'Result must only contain numeric characters.', 'string.pattern.base': 'Result must only contain numeric characters.',
'string.max': 'Result must not exceed 10 characters.' 'string.max': 'Result must not exceed 10 characters.'
}), }),
announcement_time: Joi.string() result_time: Joi.string()
.pattern(/^\d{2}:\d{2}:\d{2}$/) .pattern(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)
.required() .required()
.messages({ .messages({
'string.empty': 'Announcement time is required.', 'string.empty': 'Result time is required.',
'string.pattern.base': 'Announcement time must be in HH:MM:SS format.' '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.'
}) })
}); });

View File

@ -20,10 +20,10 @@
"raw": "{\n \"accessKey\": \"<ACCESS_KEY>\",\n \"password\": \"<PASSWORD>\"\n}" "raw": "{\n \"accessKey\": \"<ACCESS_KEY>\",\n \"password\": \"<PASSWORD>\"\n}"
}, },
"url": { "url": {
"raw": "http://localhost:3000/admin/login", "raw": "http://localhost:5500/admin/login",
"protocol": "http", "protocol": "http",
"host": ["localhost"], "host": ["localhost"],
"port": "3000", "port": "5500",
"path": ["admin", "login"] "path": ["admin", "login"]
} }
} }
@ -44,13 +44,13 @@
], ],
"body": { "body": {
"mode": "raw", "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": { "url": {
"raw": "http://localhost:3000/admin/results", "raw": "http://localhost:5500/admin/results",
"protocol": "http", "protocol": "http",
"host": ["localhost"], "host": ["localhost"],
"port": "3000", "port": "5500",
"path": ["admin", "results"] "path": ["admin", "results"]
} }
} }
@ -61,10 +61,10 @@
"method": "GET", "method": "GET",
"header": [], "header": [],
"url": { "url": {
"raw": "http://localhost:3000/api/teams", "raw": "http://localhost:5500/api/teams",
"protocol": "http", "protocol": "http",
"host": ["localhost"], "host": ["localhost"],
"port": "3000", "port": "5500",
"path": ["api", "teams"] "path": ["api", "teams"]
} }
} }
@ -85,13 +85,13 @@
], ],
"body": { "body": {
"mode": "raw", "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": { "url": {
"raw": "http://localhost:3000/admin/results/1", "raw": "http://localhost:5500/admin/results/1",
"protocol": "http", "protocol": "http",
"host": ["localhost"], "host": ["localhost"],
"port": "3000", "port": "5500",
"path": ["admin", "results", "1"] "path": ["admin", "results", "1"]
} }
} }
@ -107,10 +107,10 @@
} }
], ],
"url": { "url": {
"raw": "http://localhost:3000/admin/results/1", "raw": "http://localhost:5500/admin/results/1",
"protocol": "http", "protocol": "http",
"host": ["localhost"], "host": ["localhost"],
"port": "3000", "port": "5500",
"path": ["admin", "results", "1"] "path": ["admin", "results", "1"]
} }
} }
@ -126,10 +126,10 @@
} }
], ],
"url": { "url": {
"raw": "http://localhost:3000/admin/results?team=BIKANER SUPER", "raw": "http://localhost:5500/admin/results?team=BIKANER SUPER",
"protocol": "http", "protocol": "http",
"host": ["localhost"], "host": ["localhost"],
"port": "3000", "port": "5500",
"path": ["admin", "results"], "path": ["admin", "results"],
"query": [ "query": [
{ {
@ -159,10 +159,10 @@
"raw": "{\n \"name\": \"NEW TEAM\"\n}" "raw": "{\n \"name\": \"NEW TEAM\"\n}"
}, },
"url": { "url": {
"raw": "http://localhost:3000/admin/teams", "raw": "http://localhost:5500/admin/teams",
"protocol": "http", "protocol": "http",
"host": ["localhost"], "host": ["localhost"],
"port": "3000", "port": "5500",
"path": ["admin", "teams"] "path": ["admin", "teams"]
} }
} }
@ -186,10 +186,10 @@
"raw": "{\n \"name\": \"UPDATED TEAM\"\n}" "raw": "{\n \"name\": \"UPDATED TEAM\"\n}"
}, },
"url": { "url": {
"raw": "http://localhost:3000/admin/teams/1", "raw": "http://localhost:5500/admin/teams/1",
"protocol": "http", "protocol": "http",
"host": ["localhost"], "host": ["localhost"],
"port": "3000", "port": "5500",
"path": ["admin", "teams", "1"] "path": ["admin", "teams", "1"]
} }
} }
@ -205,10 +205,10 @@
} }
], ],
"url": { "url": {
"raw": "http://localhost:3000/admin/teams/1", "raw": "http://localhost:5500/admin/teams/1",
"protocol": "http", "protocol": "http",
"host": ["localhost"], "host": ["localhost"],
"port": "3000", "port": "5500",
"path": ["admin", "teams", "1"] "path": ["admin", "teams", "1"]
} }
} }
@ -219,10 +219,10 @@
"method": "GET", "method": "GET",
"header": [], "header": [],
"url": { "url": {
"raw": "http://localhost:3000/api/today", "raw": "http://localhost:5500/api/today",
"protocol": "http", "protocol": "http",
"host": ["localhost"], "host": ["localhost"],
"port": "3000", "port": "5500",
"path": ["api", "today"] "path": ["api", "today"]
} }
} }
@ -242,10 +242,10 @@
"raw": "{\n \"team\": \"BIKANER SUPER\",\n \"month\": \"2025-03\"\n}" "raw": "{\n \"team\": \"BIKANER SUPER\",\n \"month\": \"2025-03\"\n}"
}, },
"url": { "url": {
"raw": "http://localhost:3000/api/results/monthly", "raw": "http://localhost:5500/api/results/monthly",
"protocol": "http", "protocol": "http",
"host": ["localhost"], "host": ["localhost"],
"port": "3000", "port": "5500",
"path": ["api", "results", "monthly"] "path": ["api", "results", "monthly"]
} }
} }
@ -256,10 +256,10 @@
"method": "GET", "method": "GET",
"header": [], "header": [],
"url": { "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", "protocol": "http",
"host": ["localhost"], "host": ["localhost"],
"port": "3000", "port": "5500",
"path": ["api", "results", "daily"], "path": ["api", "results", "daily"],
"query": [ "query": [
{ {
@ -271,29 +271,22 @@
} }
}, },
{ {
"name": "Publish Result Validation Example", "name": "Get Results By Team (Public)",
"request": { "request": {
"method": "POST", "method": "GET",
"header": [ "header": [],
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer <SESSION_TOKEN>"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"team\": \"INVALID@TEAM\",\n \"date\": \"2025-03-12\",\n \"result\": \"INVALID_RESULT\",\n \"announcement_time\": \"INVALID_TIME\"\n}"
},
"url": { "url": {
"raw": "http://localhost:3000/admin/results", "raw": "http://localhost:5500/api/results/team?team=BIKANER SUPER",
"protocol": "http", "protocol": "http",
"host": ["localhost"], "host": ["localhost"],
"port": "3000", "port": "5500",
"path": ["admin", "results"] "path": ["api", "results", "team"],
"query": [
{
"key": "team",
"value": "BIKANER SUPER"
}
]
} }
} }
} }

View File

@ -10,14 +10,13 @@ Kings Backend API is a RESTful API for managing teams, publishing match/show res
## Installation ## Installation
1. **Clone the Repository** 1. **Clone the Repository**
``` ```bash
git clone <repository_url> git clone <repository_url>
cd kingproject/bazar3 cd bazar3/server
``` ```
2. **Install Dependencies** 2. **Install Dependencies**
``` ```bash
cd server
npm install npm install
``` ```
@ -26,14 +25,14 @@ Kings Backend API is a RESTful API for managing teams, publishing match/show res
1. **Environment Variables** 1. **Environment Variables**
Create a `.env` file in the `/server` directory with the following variables: Create a `.env` file in the `/server` directory with the following variables:
``` ```env
DB_HOST=localhost DB_HOST=localhost
DB_USER=user DB_USER=user
DB_PASS=password DB_PASS=password
DB_NAME=kingdb_prod DB_NAME=kingdb_prod
IP_PEPPER=your_ip_pepper IP_PEPPER=your_ip_pepper
JWT_SECRET=<your_jwt_secret> JWT_SECRET=<your_jwt_secret>
PORT=3000 PORT=5500
``` ```
## Database Setup ## Database Setup
@ -41,7 +40,7 @@ Kings Backend API is a RESTful API for managing teams, publishing match/show res
1. **Import Schema** 1. **Import Schema**
Run the following command in your MySQL client to create the database and tables: Run the following command in your MySQL client to create the database and tables:
``` ```bash
mysql -u user -p < server/schema.sql mysql -u user -p < server/schema.sql
``` ```
This creates the `kingdb_prod` database and the required tables: `teams`, `results`, and `admins`. 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 ## Admin Account Setup
To create an admin account, run: To create an admin account, run:
``` ```bash
npm run create-admin -- <your_password> npm run create-admin -- <your_password>
``` ```
This script will output an `Access Key` for admin login. 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 ## Running the Server
Start the API server by running: Start the API server by running:
``` ```bash
npm start 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 ## API Endpoints
### Public Endpoints ### Public Endpoints
- **GET /api/results?team=&lt;TEAM_NAME&gt;&date=&lt;YYYY-MM-DD&gt;** - **GET /api/results?team=<TEAM_NAME>&date=<YYYY-MM-DD>**
Retrieve the result for a specified team and date. Retrieve the result for a specified team and date.
- **GET /api/today** - **GET /api/today**
Retrieve all results for the current day. 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" "month": "2025-03"
} }
``` ```
- **GET /api/results/daily?date=&lt;YYYY-MM-DD&gt;** - **GET /api/results/daily?date=<YYYY-MM-DD>**
Get daily results for all teams. Get daily results for all teams.
### Admin Endpoints ### Admin Endpoints
@ -99,8 +98,8 @@ The server will listen on the port specified in your `.env` file (default is 300
```json ```json
{ {
"team": "NEW TEAM", "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** - **GET /api/teams**
Retrieve all teams (public). Retrieve all teams (public).
- **POST /api/teams** - **POST /admin/teams**
Create a new team (admin only). Create a new team (admin only).
_Request Body Example:_
```json ```json
{ {
"name": "NEW TEAM" "name": "NEW TEAM"
} }
``` ```
- **PUT /api/teams/:id** - **PUT /admin/teams/:id**
Update a team (admin only, requires Bearer token). 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). Delete a team (admin only, requires Bearer token).
### Testing Sanitization ### Testing the API
A sample endpoint (POST /api/teams) will sanitize HTML input. For example, sending:
```json
{
"name": "<script>alert('xss');</script>"
}
```
will have the `<` and `>` characters escaped to protect against XSS.
## Testing the API
1. **Using Postman** 1. **Using Postman**
@ -140,7 +137,7 @@ will have the `<` and `>` characters escaped to protect against XSS.
2. **Using the Test Script** 2. **Using the Test Script**
A test script is available that performs a sequence of API calls: A test script is available that performs a sequence of API calls:
``` ```bash
npm run test-api npm run test-api
``` ```
This script uses `axios` to: This script uses `axios` to:

View File

@ -1,75 +1,76 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const adminController = require('../controllers/adminController'); const adminService = require('../services/adminService');
const { validateResult } = require('../middlewares/validation'); const { validateResult } = require('../middlewares/validation');
const { authorizeAdmin } = require('../middlewares/authorization');
router.post('/login', async (req, res, next) => { router.post('/login', async (req, res, next) => {
try { try {
const { accessKey, password } = req.body; const { accessKey, password } = req.body;
const token = await adminController.login(accessKey, password); const token = await adminService.login(accessKey, password);
res.json({ token }); res.json({ token });
} catch (error) { } catch (error) {
next(error); next(error);
} }
}); });
router.post('/results', validateResult, async (req, res, next) => { router.post('/results', authorizeAdmin, validateResult, async (req, res, next) => {
try { try {
await adminController.publishResult(req.body, req.headers.authorization); await adminService.publishResult(req.body);
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {
next(error); next(error);
} }
}); });
router.put('/results/:id', validateResult, async (req, res, next) => { router.put('/results/:id', authorizeAdmin, validateResult, async (req, res, next) => {
try { try {
await adminController.updateResultById(req.params.id, req.body, req.headers.authorization); await adminService.updateResultById(req.params.id, req.body);
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {
next(error); next(error);
} }
}); });
router.delete('/results/:id', async (req, res, next) => { router.delete('/results/:id', authorizeAdmin, async (req, res, next) => {
try { try {
await adminController.deleteResultById(req.params.id, req.headers.authorization); await adminService.deleteResultById(req.params.id);
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {
next(error); next(error);
} }
}); });
router.get('/results', async (req, res, next) => { router.get('/results', authorizeAdmin, async (req, res, next) => {
try { try {
const data = await adminController.getResultsByTeam(req.query.team, req.headers.authorization); const data = await adminService.getResultsByTeam(req.query.team);
res.json(data); res.json(data);
} catch (error) { } catch (error) {
next(error); next(error);
} }
}); });
router.post('/teams', async (req, res, next) => { router.post('/teams', authorizeAdmin, async (req, res, next) => {
try { try {
const result = await adminController.createTeam(req.body, req.headers.authorization); const result = await adminService.createTeam(req.body);
res.json(result); res.json(result);
} catch (error) { } catch (error) {
next(error); next(error);
} }
}); });
router.put('/teams/:id', async (req, res, next) => { router.put('/teams/:id', authorizeAdmin, async (req, res, next) => {
try { 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); res.json(result);
} catch (error) { } catch (error) {
next(error); next(error);
} }
}); });
router.delete('/teams/:id', async (req, res, next) => { router.delete('/teams/:id', authorizeAdmin, async (req, res, next) => {
try { try {
const result = await adminController.deleteTeam(req.params.id, req.headers.authorization); const result = await adminService.deleteTeam(req.params.id);
res.json(result); res.json(result);
} catch (error) { } catch (error) {
next(error); next(error);

View File

@ -1,75 +1,64 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const db = require('../db'); const resultService = require('../services/resultService');
const cache = require('../cache'); const { validateDate, validateMonthlyResults } = require('../middlewares/validation');
const resultController = require('../controllers/resultController');
router.get('/results', async (req, res) => { router.get('/results', async (req, res, next) => {
try { try {
const { team, date } = req.query; const { team, date } = req.query;
const cacheKey = `${team}:${date}`; const results = await resultService.getResultsByTeamAndDate(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);
res.json(results); res.json(results);
} catch (error) { } catch (error) {
res.status(500).json({ error: 'Server error' }); next(error);
} }
}); });
// health check router.get('/today', async (req, res, next) => {
router.get('/health', async (req, res) => {
try { try {
await db.query('SELECT 1'); // Simple query to check DB connection const results = await resultService.getTodayResults();
res.json({ status: 'healthy' }); res.json(results);
} catch (error) { } catch (error) {
res.status(500).json({ status: 'unhealthy', error: error.message }); next(error);
} }
}); });
// (expects body: { team, month }) router.get('/health', async (req, res, next) => {
router.post('/results/monthly', resultController.getMonthlyResults); try {
const healthStatus = await resultService.checkHealth();
res.json(healthStatus);
} catch (error) {
next(error);
}
});
// (expects query param: date=YYYY-MM-DD) router.post('/results/monthly', validateMonthlyResults, async (req, res, next) => {
router.get('/results/daily', resultController.getDailyResults); 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; module.exports = router;

View File

@ -1,7 +1,7 @@
const express = require('express'); const express = require('express');
const router = express.Router(); 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; module.exports = router;

View File

@ -5,17 +5,16 @@ CREATE TABLE teams (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE, name VARCHAR(100) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); ) ENGINE=InnoDB;
CREATE TABLE results ( CREATE TABLE results (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
team_id INT NOT NULL, team_id INT NOT NULL,
result_date DATE NOT NULL, result_time DATETIME NOT NULL, -- When the result will be shown
announcement_time TIME NOT NULL, result VARCHAR(10) NOT NULL DEFAULT '-1', -- Default result if admin hasn't set it
result VARCHAR(10) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE, FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE
UNIQUE KEY uniq_team_date (team_id, result_date) ) ENGINE=InnoDB;
);
CREATE TABLE admins ( CREATE TABLE admins (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
@ -23,5 +22,5 @@ CREATE TABLE admins (
argon2_hash TEXT NOT NULL, argon2_hash TEXT NOT NULL,
session_token CHAR(64), session_token CHAR(64),
is_active BOOLEAN DEFAULT TRUE, is_active BOOLEAN DEFAULT TRUE,
last_access TIMESTAMP last_access TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
); ) ENGINE=InnoDB;

View File

@ -54,8 +54,7 @@ const BASE_URL = 'http://localhost:3000';
{ {
team: 'NEW TEAM', team: 'NEW TEAM',
date: '2025-03-12', date: '2025-03-12',
result: '45', result: '45'
announcement_time: '02:30:00'
}, },
{ {
headers: { Authorization: `Bearer ${sessionToken}` } headers: { Authorization: `Bearer ${sessionToken}` }

View File

@ -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]);
};

View File

@ -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;
};

View File

@ -1,8 +1,14 @@
const db = require('../db'); const db = require('../db');
exports.getAllTeams = async (req, res) => { exports.getAllTeams = async (req, res) => {
console.log('Fetching all teams...');
try { try {
const teams = await db.query('SELECT * FROM teams'); 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); res.json(teams);
} catch (error) { } catch (error) {
console.error('Error fetching teams:', error); console.error('Error fetching teams:', error);