first commit
Some checks failed
Build Images and Deploy / Update-PROD-Stack (push) Failing after 14s

This commit is contained in:
2026-02-28 00:01:41 -05:00
commit 4255d95c68
36 changed files with 4665 additions and 0 deletions

246
src/models/index.js Normal file
View 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 };