mirror of
https://github.com/itsnaveenk/bazar3.git
synced 2025-12-19 21:17:06 +00:00
major changes
This commit is contained in:
parent
021a7ada25
commit
9cc08debed
@ -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();
|
||||
@ -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]);
|
||||
};
|
||||
@ -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' });
|
||||
}
|
||||
};
|
||||
19
server/middlewares/authorization.js
Normal file
19
server/middlewares/authorization.js
Normal 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.' });
|
||||
}
|
||||
};
|
||||
@ -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.'
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@ -20,10 +20,10 @@
|
||||
"raw": "{\n \"accessKey\": \"<ACCESS_KEY>\",\n \"password\": \"<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 <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}"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 <repository_url>
|
||||
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=<your_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 -- <your_password>
|
||||
```
|
||||
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=<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.
|
||||
@ -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=<YYYY-MM-DD>**
|
||||
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**
|
||||
- **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": "<script>alert('xss');</script>"
|
||||
}
|
||||
```
|
||||
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:
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
);
|
||||
last_access TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
@ -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}` }
|
||||
|
||||
95
server/services/adminService.js
Normal file
95
server/services/adminService.js
Normal 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]);
|
||||
};
|
||||
135
server/services/resultService.js
Normal file
135
server/services/resultService.js
Normal 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;
|
||||
};
|
||||
@ -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);
|
||||
Loading…
x
Reference in New Issue
Block a user