adding admin options for pw reset

This commit is contained in:
2026-01-30 22:15:31 -05:00
parent c2a6e1d41f
commit b7b32b4fe6
9 changed files with 678 additions and 1 deletions

View File

@@ -5,6 +5,7 @@ CREATE TABLE IF NOT EXISTS users (
email VARCHAR(255) UNIQUE NOT NULL,
username VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
is_admin BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_email (email),
INDEX idx_username (username)
@@ -76,3 +77,17 @@ CREATE TABLE IF NOT EXISTS tmdb_cache (
UNIQUE KEY unique_query (query, media_type),
INDEX idx_query (query)
);
CREATE TABLE IF NOT EXISTS password_reset_tokens (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
token VARCHAR(255) UNIQUE NOT NULL,
created_by INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
used_at TIMESTAMP NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_token (token),
INDEX idx_user_id (user_id)
);

View File

@@ -16,6 +16,7 @@ import predictionRoutes from './routes/predictions.js';
import friendRoutes from './routes/friends.js';
import tmdbRoutes from './routes/tmdb.js';
import leaderboardRoutes from './routes/leaderboard.js';
import adminRoutes from './routes/admin.js';
dotenv.config();
@@ -67,6 +68,7 @@ app.use('/api/predictions', apiLimiter, predictionRoutes);
app.use('/api/friends', apiLimiter, friendRoutes);
app.use('/api/tmdb', tmdbLimiter, tmdbRoutes);
app.use('/api/leaderboard', apiLimiter, leaderboardRoutes);
app.use('/api/admin', apiLimiter, adminRoutes);
// Health check
app.get('/api/health', (req, res) => {

View File

@@ -1,4 +1,6 @@
import jwt from 'jsonwebtoken';
import { query } from '../db/index.js';
import { asyncHandler } from './errorHandler.js';
export const authMiddleware = (req, res, next) => {
const authHeader = req.headers.authorization;
@@ -17,3 +19,35 @@ export const authMiddleware = (req, res, next) => {
return res.status(401).json({ error: 'Invalid token' });
}
};
// Enhanced version that fetches user data including admin status
export const verifyToken = asyncHandler(async (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.substring(7);
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Fetch full user data including admin status
const users = await query(
'SELECT id, email, username, is_admin FROM users WHERE id = ?',
[decoded.userId]
);
if (users.length === 0) {
return res.status(401).json({ error: 'User not found' });
}
req.user = {
userId: users[0].id,
email: users[0].email,
username: users[0].username,
is_admin: users[0].is_admin
};
next();
});

View File

@@ -0,0 +1,91 @@
import express from 'express';
import crypto from 'crypto';
import { query } from '../db/index.js';
import { asyncHandler, AppError } from '../middleware/errorHandler.js';
import { verifyToken } from '../middleware/auth.js';
const router = express.Router();
// Middleware to check if user is admin
const requireAdmin = asyncHandler(async (req, res, next) => {
if (!req.user || !req.user.is_admin) {
throw new AppError('Admin access required', 403);
}
next();
});
// Get all users (admin only)
router.get('/users', verifyToken, requireAdmin, asyncHandler(async (req, res) => {
const users = await query(
'SELECT id, email, username, is_admin, created_at FROM users ORDER BY created_at DESC'
);
res.json({ users });
}));
// Generate password reset token (admin only)
router.post('/generate-reset-token', verifyToken, requireAdmin, asyncHandler(async (req, res) => {
const { userId } = req.body;
if (!userId) {
throw new AppError('User ID required', 400);
}
// Verify user exists
const users = await query('SELECT id, username, email FROM users WHERE id = ?', [userId]);
if (users.length === 0) {
throw new AppError('User not found', 404);
}
const user = users[0];
// Generate secure token
const token = crypto.randomBytes(32).toString('hex');
// Token expires in 1 hour
const expiresAt = new Date(Date.now() + 60 * 60 * 1000);
// Store token
await query(
'INSERT INTO password_reset_tokens (user_id, token, created_by, expires_at) VALUES (?, ?, ?, ?)',
[userId, token, req.user.userId, expiresAt]
);
// Generate reset URL
const resetUrl = `${process.env.FRONTEND_URL || 'http://localhost:5173'}/reset-password/${token}`;
res.json({
success: true,
resetUrl,
user: {
id: user.id,
username: user.username,
email: user.email
},
expiresAt
});
}));
// Get reset token history (admin only)
router.get('/reset-tokens', verifyToken, requireAdmin, asyncHandler(async (req, res) => {
const tokens = await query(`
SELECT
prt.id,
prt.token,
prt.created_at,
prt.expires_at,
prt.used_at,
u.id as user_id,
u.username,
u.email,
admin.username as created_by_username
FROM password_reset_tokens prt
JOIN users u ON prt.user_id = u.id
JOIN users admin ON prt.created_by = admin.id
ORDER BY prt.created_at DESC
LIMIT 100
`);
res.json({ tokens });
}));
export default router;

View File

@@ -95,7 +95,7 @@ router.get('/me', asyncHandler(async (req, res) => {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const users = await query(
'SELECT id, email, username, created_at FROM users WHERE id = ?',
'SELECT id, email, username, is_admin, created_at FROM users WHERE id = ?',
[decoded.userId]
);
@@ -106,4 +106,56 @@ router.get('/me', asyncHandler(async (req, res) => {
res.json({ user: users[0] });
}));
// Reset password with token (public route)
router.post('/reset-password', asyncHandler(async (req, res) => {
const { token, newPassword } = req.body;
if (!token || !newPassword) {
throw new AppError('Token and new password required', 400);
}
if (newPassword.length < 6) {
throw new AppError('Password must be at least 6 characters', 400);
}
// Find valid token
const tokens = await query(
`SELECT prt.*, u.email, u.username
FROM password_reset_tokens prt
JOIN users u ON prt.user_id = u.id
WHERE prt.token = ? AND prt.used_at IS NULL AND prt.expires_at > NOW()`,
[token]
);
if (tokens.length === 0) {
throw new AppError('Invalid or expired reset token', 400);
}
const resetToken = tokens[0];
// Hash new password
const password_hash = await bcrypt.hash(newPassword, 10);
// Update password
await query(
'UPDATE users SET password_hash = ? WHERE id = ?',
[password_hash, resetToken.user_id]
);
// Mark token as used
await query(
'UPDATE password_reset_tokens SET used_at = NOW() WHERE id = ?',
[resetToken.id]
);
res.json({
success: true,
message: 'Password reset successfully',
user: {
email: resetToken.email,
username: resetToken.username
}
});
}));
export default router;