This commit is contained in:
Naveen Kumar 2025-03-17 20:40:48 +05:30
commit c1433aa070
19 changed files with 2519 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
node_modules/
.env
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.DS_Store
.idea/
dist/
coverage/

39
server/auth.js Normal file
View File

@ -0,0 +1,39 @@
const argon2 = require('argon2');
const speakeasy = require('speakeasy');
// Generate Admin Credentials
const createAdmin = async (password) => {
const accessKey = crypto.randomBytes(16).toString('hex');
const hash = await argon2.hash(password);
const totpSecret = speakeasy.generateSecret({ length: 20 });
return {
accessKey,
hash,
totpSecret: totpSecret.base32
};
};
// Verify Admin Login
const verifyAdmin = async (accessKey, password, token) => {
// 1. Fetch admin from DB
const { rows: [admin] } = await db.query(
'SELECT * FROM admins WHERE access_key = $1',
[accessKey]
);
// 2. Verify password
if (!admin || !await argon2.verify(admin.argon2_hash, password)) {
return false;
}
// 3. Verify TOTP
return speakeasy.totp.verify({
secret: admin.totp_secret,
encoding: 'base32',
token,
window: 1
});
};
module.exports = { createAdmin, verifyAdmin };

31
server/cache.js Normal file
View File

@ -0,0 +1,31 @@
class SattaCache {
constructor() {
this.store = new Map();
this.ttl = 300000;
}
set(key, value) {
this.store.set(key, { data: value, expires: Date.now() + this.ttl });
}
get(key) {
const entry = this.store.get(key);
if (entry && Date.now() < entry.expires) return entry.data;
this.store.delete(key);
return null;
}
has(key) {
return this.store.has(key) && Date.now() < this.store.get(key).expires;
}
delete(key) {
this.store.delete(key);
}
clear() {
this.store.clear();
}
}
module.exports = new SattaCache();

View File

@ -0,0 +1,37 @@
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 } = data;
await db.query(`
INSERT INTO results (team_id, result_date, result)
SELECT id, ?, ? FROM teams WHERE name = ?
ON DUPLICATE KEY UPDATE result = VALUES(result)
`, [date, result, team.toUpperCase()]);
};

View File

@ -0,0 +1,81 @@
const db = require('../db');
// Get all teams
exports.getAllTeams = async (req, res) => {
try {
const teams = await db.query('SELECT * FROM teams');
res.json(teams);
} catch (error) {
console.error('Error fetching teams:', error);
res.status(500).json({ error: 'Failed to fetch teams' });
}
};
// Create a new team
exports.createTeam = async (req, res) => {
try {
const { name, announcement_time } = req.body;
if (!name || !announcement_time) {
return res.status(400).json({ error: 'Name and announcement time are required' });
}
await db.query(
'INSERT INTO teams (name, announcement_time) VALUES (?, ?)',
[name.toUpperCase(), announcement_time]
);
res.status(201).json({ success: true, message: 'Team created successfully' });
} catch (error) {
console.error('Error creating team:', error);
res.status(500).json({ error: 'Failed to create team' });
}
};
// Update a team
exports.updateTeam = async (req, res) => {
try {
const { id } = req.params;
const { name, announcement_time } = req.body;
if (!name && !announcement_time) {
return res.status(400).json({ error: 'At least one field (name or announcement time) is required' });
}
const fields = [];
const values = [];
if (name) {
fields.push('name = ?');
values.push(name.toUpperCase());
}
if (announcement_time) {
fields.push('announcement_time = ?');
values.push(announcement_time);
}
values.push(id);
await db.query(`UPDATE teams SET ${fields.join(', ')} WHERE id = ?`, values);
res.json({ success: true, message: 'Team updated successfully' });
} catch (error) {
console.error('Error updating team:', error);
res.status(500).json({ error: 'Failed to update team' });
}
};
// Delete a team
exports.deleteTeam = async (req, res) => {
try {
const { id } = req.params;
await db.query('DELETE FROM teams WHERE id = ?', [id]);
res.json({ success: true, message: 'Team deleted successfully' });
} catch (error) {
console.error('Error deleting team:', error);
res.status(500).json({ error: 'Failed to delete team' });
}
};

19
server/db.js Normal file
View File

@ -0,0 +1,19 @@
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
waitForConnections: true,
connectionLimit: 20,
queueLimit: 0,
timezone: '+00:00'
});
module.exports = {
query: async (sql, params) => {
const [rows] = await pool.execute(sql, params);
return rows;
}
};

View File

@ -0,0 +1,14 @@
const { RateLimiterMemory } = require('rate-limiter-flexible');
const publicLimiter = new RateLimiterMemory({
points: 100,
duration: 60
});
module.exports = {
publicLimiter: (req, res, next) => {
publicLimiter.consume(req.anonymizedIP)
.then(() => next())
.catch(() => res.status(429).json({ error: 'Too many requests' }));
}
};

View File

@ -0,0 +1,17 @@
const crypto = require('crypto');
module.exports = {
// IP Anonymization
anonymizeIP: (req, res, next) => {
const ip = req.ip || '127.0.0.1';
const salt = Math.floor(Date.now() / 3600000); // Hourly salt
req.anonymizedIP = crypto.createHash('sha3-256')
.update(ip + salt + process.env.IP_PEPPER)
.digest('hex');
next();
},
sanitizeInput: (req, res, next) => {
// ...implement input sanitization if needed...
next();
}
};

View File

@ -0,0 +1,28 @@
const Joi = require('joi');
exports.validateTeam = (req, res, next) => {
const schema = Joi.object({
name: Joi.string().max(100).required(),
announcement_time: Joi.string().pattern(/^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/).required()
});
const { error } = schema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
next();
};
exports.validateResult = (req, res, next) => {
const schema = Joi.object({
team: Joi.string().max(100).required(),
date: Joi.string().pattern(/^\d{4}-\d{2}-\d{2}$/).required(),
result: Joi.string().max(10).required()
});
const { error } = schema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
next();
};

1788
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
server/package.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "satta-backend",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js",
"create-admin": "node scripts/create-admin.js",
"test-api": "node scripts/test-api.js"
},
"dependencies": {
"argon2": "^0.31.0",
"axios": "^1.8.3",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"joi": "^17.9.2",
"mysql2": "^3.6.5",
"rate-limiter-flexible": "^2.4.2"
}
}

View File

@ -0,0 +1,161 @@
{
"info": {
"_postman_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Satta Backend API",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "Admin Login",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"accessKey\": \"<ACCESS_KEY>\",\n \"password\": \"<PASSWORD>\"\n}"
},
"url": {
"raw": "http://localhost:3000/admin/login",
"protocol": "http",
"host": ["localhost"],
"port": "3000",
"path": ["admin", "login"]
}
}
},
{
"name": "Publish Result",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer <SESSION_TOKEN>"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"team\": \"BIKANER SUPER\",\n \"date\": \"2025-03-12\",\n \"result\": \"45\"\n}"
},
"url": {
"raw": "http://localhost:3000/admin/results",
"protocol": "http",
"host": ["localhost"],
"port": "3000",
"path": ["admin", "results"]
}
}
},
{
"name": "Get All Teams",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:3000/api/teams",
"protocol": "http",
"host": ["localhost"],
"port": "3000",
"path": ["api", "teams"]
}
}
},
{
"name": "Create Team",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"name\": \"NEW TEAM\",\n \"announcement_time\": \"02:30:00\"\n}"
},
"url": {
"raw": "http://localhost:3000/api/teams",
"protocol": "http",
"host": ["localhost"],
"port": "3000",
"path": ["api", "teams"]
}
}
},
{
"name": "Update Team",
"request": {
"method": "PUT",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"name\": \"UPDATED TEAM\",\n \"announcement_time\": \"03:00:00\"\n}"
},
"url": {
"raw": "http://localhost:3000/api/teams/1",
"protocol": "http",
"host": ["localhost"],
"port": "3000",
"path": ["api", "teams", "1"]
}
}
},
{
"name": "Delete Team",
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "http://localhost:3000/api/teams/1",
"protocol": "http",
"host": ["localhost"],
"port": "3000",
"path": ["api", "teams", "1"]
}
}
},
{
"name": "Get Today's Results",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:3000/api/today",
"protocol": "http",
"host": ["localhost"],
"port": "3000",
"path": ["api", "today"]
}
}
},
{
"name": "Health Check",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:3000/api/health",
"protocol": "http",
"host": ["localhost"],
"port": "3000",
"path": ["api", "health"]
}
}
}
]
}

25
server/routes/admin.js Normal file
View File

@ -0,0 +1,25 @@
const express = require('express');
const router = express.Router();
const adminController = require('../controllers/adminController');
const { validateResult } = require('../middlewares/validation');
router.post('/login', async (req, res, next) => {
try {
const { accessKey, password } = req.body;
const token = await adminController.login(accessKey, password);
res.json({ token });
} catch (error) {
next(error);
}
});
router.post('/results', validateResult, async (req, res, next) => {
try {
await adminController.publishResult(req.body, req.headers.authorization);
res.json({ success: true });
} catch (error) {
next(error);
}
});
module.exports = router;

66
server/routes/public.js Normal file
View File

@ -0,0 +1,66 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const cache = require('../cache');
// Get specific result
router.get('/results', async (req, res) => {
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, t.name AS team
FROM results r
JOIN teams t ON r.team_id = t.id
WHERE t.name = ? AND r.result_date = ?
`, [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' });
}
});
// Get all today's results
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
FROM results r
JOIN teams t ON r.team_id = t.id
WHERE r.result_date = ?
`, [today]);
cache.set(cacheKey, results);
res.json(results);
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
});
// health check
router.get('/health', async (req, res) => {
try {
await db.query('SELECT 1'); // Simple query to check DB connection
res.json({ status: 'healthy' });
} catch (error) {
res.status(500).json({ status: 'unhealthy', error: error.message });
}
});
module.exports = router;

18
server/routes/team.js Normal file
View File

@ -0,0 +1,18 @@
const express = require('express');
const router = express.Router();
const teamController = require('../controllers/teamController');
const { validateTeam } = require('../middlewares/validation');
// Get all teams
router.get('/', teamController.getAllTeams);
// Create a new team
router.post('/', validateTeam, teamController.createTeam);
// Update a team
router.put('/:id', validateTeam, teamController.updateTeam);
// Delete a team
router.delete('/:id', teamController.deleteTeam);
module.exports = router;

27
server/schema.sql Normal file
View File

@ -0,0 +1,27 @@
CREATE DATABASE IF NOT EXISTS satta_prod;
USE satta_prod;
CREATE TABLE teams (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE,
announcement_time TIME NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE results (
id INT AUTO_INCREMENT PRIMARY KEY,
team_id INT NOT NULL,
result_date DATE 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)
);
CREATE TABLE admins (
id INT AUTO_INCREMENT PRIMARY KEY,
access_key CHAR(64) UNIQUE NOT NULL,
argon2_hash TEXT NOT NULL,
session_token CHAR(64),
is_active BOOLEAN DEFAULT TRUE,
last_access TIMESTAMP
);

View File

@ -0,0 +1,28 @@
require('dotenv').config();
const crypto = require('crypto');
const argon2 = require('argon2');
const db = require('../db');
(async () => {
try {
const accessKey = crypto.randomBytes(16).toString('hex');
const password = process.argv[2];
if (!password) {
console.error('Usage: node scripts/create-admin.js <password>');
process.exit(1);
}
const hash = await argon2.hash(password);
await db.query(
`INSERT INTO admins (access_key, argon2_hash) VALUES (?, ?)`,
[accessKey, hash]
);
console.log('Admin created successfully!');
console.log(`Access Key: ${accessKey}`);
} catch (error) {
console.error('Error creating admin:', error);
} finally {
process.exit();
}
})();

View File

@ -0,0 +1,73 @@
const axios = require('axios');
const BASE_URL = 'http://localhost:3000';
(async () => {
try {
// Admin Login
console.log('Logging in as admin...');
const loginResponse = await axios.post(`${BASE_URL}/admin/login`, {
accessKey: '<ACCESS_KEY>',
password: '<PASSWORD>'
});
const sessionToken = loginResponse.data.token;
console.log('Login successful. Session Token:', sessionToken);
// Create a Team
console.log('Creating a new team...');
const createTeamResponse = await axios.post(
`${BASE_URL}/api/teams`,
{
name: 'NEW TEAM',
announcement_time: '02:30:00'
},
{
headers: { Authorization: `Bearer ${sessionToken}` }
}
);
console.log('Team created:', createTeamResponse.data);
// Get All Teams
console.log('Fetching all teams...');
const teamsResponse = await axios.get(`${BASE_URL}/api/teams`);
console.log('Teams:', teamsResponse.data);
// Update a Team
console.log('Updating a team...');
const updateTeamResponse = await axios.put(
`${BASE_URL}/api/teams/1`,
{
name: 'UPDATED TEAM',
announcement_time: '03:00:00'
},
{
headers: { Authorization: `Bearer ${sessionToken}` }
}
);
console.log('Team updated:', updateTeamResponse.data);
// Delete a Team
console.log('Deleting a team...');
const deleteTeamResponse = await axios.delete(`${BASE_URL}/api/teams/1`, {
headers: { Authorization: `Bearer ${sessionToken}` }
});
console.log('Team deleted:', deleteTeamResponse.data);
// Publish a Result
console.log('Publishing a result...');
const publishResultResponse = await axios.post(
`${BASE_URL}/admin/results`,
{
team: 'NEW TEAM',
date: '2025-03-12',
result: '45'
},
{
headers: { Authorization: `Bearer ${sessionToken}` }
}
);
console.log('Result published:', publishResultResponse.data);
} catch (error) {
console.error('Error:', error.response?.data || error.message);
}
})();

38
server/server.js Normal file
View File

@ -0,0 +1,38 @@
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const security = require('./middlewares/security');
const rateLimit = require('./middlewares/rateLimit');
const cache = require('./cache');
const publicRoutes = require('./routes/public');
const adminRoutes = require('./routes/admin');
const teamRoutes = require('./routes/team');
const app = express();
app.use(cors({ origin: ['http://localhost:3000', 'https://your-production-domain.com'] }));
app.use(express.json({ limit: '10kb' }));
app.use(security.anonymizeIP);
app.use(security.sanitizeInput);
app.use(rateLimit.publicLimiter);
app.use('/api', publicRoutes);
app.use('/admin', adminRoutes);
app.use('/api/teams', teamRoutes);
app.use((req, res, next) => {
if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
cache.store.clear();
}
next();
});
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({ error: err.message || 'Internal Server Error' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});