first commit
Some checks failed
Build Images and Deploy / Update-PROD-Stack (push) Failing after 14s
Some checks failed
Build Images and Deploy / Update-PROD-Stack (push) Failing after 14s
This commit is contained in:
246
src/models/index.js
Normal file
246
src/models/index.js
Normal file
@@ -0,0 +1,246 @@
|
||||
const db = require('../config/database');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const crypto = require('crypto');
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────
|
||||
function generateCode(length = 5) {
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // no I/O/0/1 to avoid confusion
|
||||
let code = '';
|
||||
const bytes = crypto.randomBytes(length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
code += chars[bytes[i] % chars.length];
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
function getPointsForScanNumber(scanNumber) {
|
||||
if (scanNumber === 1) return 500;
|
||||
if (scanNumber === 2) return 250;
|
||||
if (scanNumber === 3) return 100;
|
||||
return 50;
|
||||
}
|
||||
|
||||
// ─── Users ────────────────────────────────────────────────
|
||||
const Users = {
|
||||
create(username, password) {
|
||||
const hash = bcrypt.hashSync(password, 12);
|
||||
const stmt = db.prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)');
|
||||
const result = stmt.run(username, hash);
|
||||
return result.lastInsertRowid;
|
||||
},
|
||||
|
||||
findByUsername(username) {
|
||||
return db.prepare('SELECT * FROM users WHERE username = ?').get(username);
|
||||
},
|
||||
|
||||
findById(id) {
|
||||
return db.prepare('SELECT id, username, is_admin, created_at FROM users WHERE id = ?').get(id);
|
||||
},
|
||||
|
||||
verifyPassword(user, password) {
|
||||
return bcrypt.compareSync(password, user.password_hash);
|
||||
},
|
||||
|
||||
makeAdmin(userId) {
|
||||
db.prepare('UPDATE users SET is_admin = 1 WHERE id = ?').run(userId);
|
||||
},
|
||||
|
||||
getTotalPoints(userId) {
|
||||
const row = db.prepare('SELECT COALESCE(SUM(points_awarded), 0) as total FROM scans WHERE user_id = ?').get(userId);
|
||||
return row.total;
|
||||
},
|
||||
|
||||
getProfile(userId) {
|
||||
const user = this.findById(userId);
|
||||
if (!user) return null;
|
||||
const totalPoints = this.getTotalPoints(userId);
|
||||
const scanCount = db.prepare('SELECT COUNT(*) as count FROM scans WHERE user_id = ? AND points_awarded > 0').get(userId).count;
|
||||
return { ...user, totalPoints, scanCount };
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Hunts ────────────────────────────────────────────────
|
||||
const Hunts = {
|
||||
create(name, shortName, description, packageCount, expiryDate, createdBy) {
|
||||
const stmt = db.prepare(
|
||||
'INSERT INTO hunts (name, short_name, description, package_count, expiry_date, created_by) VALUES (?, ?, ?, ?, ?, ?)'
|
||||
);
|
||||
const result = stmt.run(name, shortName.toUpperCase(), description, packageCount, expiryDate || null, createdBy);
|
||||
const huntId = result.lastInsertRowid;
|
||||
|
||||
// Generate packages
|
||||
const insertPkg = db.prepare('INSERT INTO packages (hunt_id, card_number, unique_code) VALUES (?, ?, ?)');
|
||||
const usedCodes = new Set();
|
||||
|
||||
const insertAll = db.transaction(() => {
|
||||
for (let i = 1; i <= packageCount; i++) {
|
||||
let code;
|
||||
do {
|
||||
code = generateCode(5);
|
||||
} while (usedCodes.has(code));
|
||||
usedCodes.add(code);
|
||||
insertPkg.run(huntId, i, code);
|
||||
}
|
||||
});
|
||||
insertAll();
|
||||
|
||||
return huntId;
|
||||
},
|
||||
|
||||
findById(id) {
|
||||
return db.prepare('SELECT * FROM hunts WHERE id = ?').get(id);
|
||||
},
|
||||
|
||||
findByShortName(shortName) {
|
||||
return db.prepare('SELECT * FROM hunts WHERE short_name = ? COLLATE NOCASE').get(shortName);
|
||||
},
|
||||
|
||||
getAll() {
|
||||
return db.prepare('SELECT h.*, u.username as creator_name FROM hunts h JOIN users u ON h.created_by = u.id ORDER BY h.created_at DESC').all();
|
||||
},
|
||||
|
||||
getByCreator(userId) {
|
||||
return db.prepare('SELECT * FROM hunts WHERE created_by = ? ORDER BY created_at DESC').all(userId);
|
||||
},
|
||||
|
||||
isExpired(hunt) {
|
||||
if (!hunt.expiry_date) return false;
|
||||
return new Date(hunt.expiry_date) < new Date();
|
||||
},
|
||||
|
||||
getLeaderboard(huntId) {
|
||||
return db.prepare(`
|
||||
SELECT u.id, u.username, SUM(s.points_awarded) as total_points, COUNT(s.id) as scans
|
||||
FROM scans s
|
||||
JOIN users u ON s.user_id = u.id
|
||||
JOIN packages p ON s.package_id = p.id
|
||||
WHERE p.hunt_id = ? AND s.points_awarded > 0
|
||||
GROUP BY u.id
|
||||
ORDER BY total_points DESC
|
||||
`).all(huntId);
|
||||
},
|
||||
|
||||
shortNameExists(shortName) {
|
||||
const row = db.prepare('SELECT id FROM hunts WHERE short_name = ? COLLATE NOCASE').get(shortName);
|
||||
return !!row;
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Packages ─────────────────────────────────────────────
|
||||
const Packages = {
|
||||
findById(id) {
|
||||
return db.prepare('SELECT * FROM packages WHERE id = ?').get(id);
|
||||
},
|
||||
|
||||
findByHuntAndCode(shortName, uniqueCode) {
|
||||
return db.prepare(`
|
||||
SELECT p.*, h.name as hunt_name, h.short_name as hunt_short_name, h.id as hunt_id, h.expiry_date
|
||||
FROM packages p
|
||||
JOIN hunts h ON p.hunt_id = h.id
|
||||
WHERE h.short_name = ? COLLATE NOCASE AND p.unique_code = ? COLLATE NOCASE
|
||||
`).get(shortName, uniqueCode);
|
||||
},
|
||||
|
||||
getByHunt(huntId) {
|
||||
return db.prepare(`
|
||||
SELECT p.*,
|
||||
u1.username as first_scanner_name,
|
||||
u2.username as last_scanner_name
|
||||
FROM packages p
|
||||
LEFT JOIN users u1 ON p.first_scanned_by = u1.id
|
||||
LEFT JOIN users u2 ON p.last_scanned_by = u2.id
|
||||
WHERE p.hunt_id = ?
|
||||
ORDER BY p.card_number ASC
|
||||
`).all(huntId);
|
||||
},
|
||||
|
||||
getProfile(packageId) {
|
||||
return db.prepare(`
|
||||
SELECT p.*,
|
||||
h.name as hunt_name, h.short_name as hunt_short_name, h.id as hunt_id,
|
||||
u1.username as first_scanner_name,
|
||||
u2.username as last_scanner_name
|
||||
FROM packages p
|
||||
JOIN hunts h ON p.hunt_id = h.id
|
||||
LEFT JOIN users u1 ON p.first_scanned_by = u1.id
|
||||
LEFT JOIN users u2 ON p.last_scanned_by = u2.id
|
||||
WHERE p.id = ?
|
||||
`).get(packageId);
|
||||
},
|
||||
|
||||
getScanHistory(packageId) {
|
||||
return db.prepare(`
|
||||
SELECT s.*, u.username
|
||||
FROM scans s
|
||||
JOIN users u ON s.user_id = u.id
|
||||
WHERE s.package_id = ?
|
||||
ORDER BY s.scanned_at ASC
|
||||
`).all(packageId);
|
||||
},
|
||||
|
||||
updateFirstScanImage(packageId, imagePath) {
|
||||
db.prepare('UPDATE packages SET first_scan_image = ? WHERE id = ?').run(imagePath, packageId);
|
||||
},
|
||||
|
||||
updateLastScanHint(packageId, userId, hint) {
|
||||
db.prepare('UPDATE packages SET last_scanned_by = ?, last_scan_hint = ? WHERE id = ?').run(userId, hint, packageId);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Scans ────────────────────────────────────────────────
|
||||
const Scans = {
|
||||
hasUserScanned(packageId, userId) {
|
||||
const row = db.prepare('SELECT id FROM scans WHERE package_id = ? AND user_id = ? AND points_awarded > 0').get(packageId, userId);
|
||||
return !!row;
|
||||
},
|
||||
|
||||
recordScan(packageId, userId) {
|
||||
const pkg = Packages.findById(packageId);
|
||||
if (!pkg) return { error: 'Package not found' };
|
||||
|
||||
const alreadyScanned = this.hasUserScanned(packageId, userId);
|
||||
|
||||
if (alreadyScanned) {
|
||||
// No points, but update last_scanned_by so they can edit the hint
|
||||
db.prepare('UPDATE packages SET last_scanned_by = ? WHERE id = ?').run(userId, packageId);
|
||||
// Record the scan with 0 points
|
||||
db.prepare('INSERT INTO scans (package_id, user_id, points_awarded) VALUES (?, ?, 0)').run(packageId, userId);
|
||||
return { points: 0, alreadyScanned: true, isFirst: false };
|
||||
}
|
||||
|
||||
const scanNumber = pkg.scan_count + 1;
|
||||
const points = getPointsForScanNumber(scanNumber);
|
||||
|
||||
const doScan = db.transaction(() => {
|
||||
// Record the scan
|
||||
db.prepare('INSERT INTO scans (package_id, user_id, points_awarded) VALUES (?, ?, ?)').run(packageId, userId, points);
|
||||
|
||||
// Update package
|
||||
const isFirst = scanNumber === 1;
|
||||
if (isFirst) {
|
||||
db.prepare('UPDATE packages SET first_scanned_by = ?, last_scanned_by = ?, scan_count = ? WHERE id = ?')
|
||||
.run(userId, userId, scanNumber, packageId);
|
||||
} else {
|
||||
db.prepare('UPDATE packages SET last_scanned_by = ?, scan_count = ? WHERE id = ?')
|
||||
.run(userId, scanNumber, packageId);
|
||||
}
|
||||
|
||||
return { points, alreadyScanned: false, isFirst, scanNumber };
|
||||
});
|
||||
|
||||
return doScan();
|
||||
},
|
||||
|
||||
getGlobalLeaderboard() {
|
||||
return db.prepare(`
|
||||
SELECT u.id, u.username, SUM(s.points_awarded) as total_points, COUNT(s.id) as scans
|
||||
FROM scans s
|
||||
JOIN users u ON s.user_id = u.id
|
||||
WHERE s.points_awarded > 0
|
||||
GROUP BY u.id
|
||||
ORDER BY total_points DESC
|
||||
`).all();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { Users, Hunts, Packages, Scans, generateCode };
|
||||
Reference in New Issue
Block a user