Initial Push to GIT

This commit is contained in:
2026-03-06 11:49:48 -05:00
parent 43cba70fad
commit ee68c4704f
16 changed files with 1860 additions and 1 deletions

177
backend/src/routes/users.js Normal file
View File

@@ -0,0 +1,177 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const router = express.Router();
const { getDb, addUserToPublicGroups } = require('../models/db');
const { authMiddleware, adminMiddleware } = require('../middleware/auth');
const avatarStorage = multer.diskStorage({
destination: '/app/uploads/avatars',
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
cb(null, `avatar_${req.user.id}_${Date.now()}${ext}`);
}
});
const uploadAvatar = multer({
storage: avatarStorage,
limits: { fileSize: 2 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith('image/')) cb(null, true);
else cb(new Error('Images only'));
}
});
// List users (admin)
router.get('/', authMiddleware, adminMiddleware, (req, res) => {
const db = getDb();
const users = db.prepare(`
SELECT id, name, email, role, status, is_default_admin, must_change_password, avatar, about_me, display_name, created_at
FROM users WHERE status != 'deleted'
ORDER BY created_at ASC
`).all();
res.json({ users });
});
// Get single user profile (public-ish for mentions)
router.get('/search', authMiddleware, (req, res) => {
const { q } = req.query;
const db = getDb();
const users = db.prepare(`
SELECT id, name, display_name, avatar, role, status, hide_admin_tag FROM users
WHERE status = 'active' AND (name LIKE ? OR display_name LIKE ?)
LIMIT 10
`).all(`%${q}%`, `%${q}%`);
res.json({ users });
});
// Create user (admin)
router.post('/', authMiddleware, adminMiddleware, (req, res) => {
const { name, email, password, role } = req.body;
if (!name || !email || !password) return res.status(400).json({ error: 'Name, email, password required' });
const db = getDb();
const exists = db.prepare('SELECT id FROM users WHERE email = ?').get(email);
if (exists) return res.status(400).json({ error: 'Email already in use' });
const hash = bcrypt.hashSync(password, 10);
const result = db.prepare(`
INSERT INTO users (name, email, password, role, status, must_change_password)
VALUES (?, ?, ?, ?, 'active', 1)
`).run(name, email, hash, role === 'admin' ? 'admin' : 'member');
addUserToPublicGroups(result.lastInsertRowid);
const user = db.prepare('SELECT id, name, email, role, status, must_change_password, created_at FROM users WHERE id = ?').get(result.lastInsertRowid);
res.json({ user });
});
// Bulk create users via CSV data
router.post('/bulk', authMiddleware, adminMiddleware, (req, res) => {
const { users } = req.body; // array of {name, email, password, role}
const db = getDb();
const results = { created: [], errors: [] };
const insertUser = db.prepare(`
INSERT INTO users (name, email, password, role, status, must_change_password)
VALUES (?, ?, ?, ?, 'active', 1)
`);
const transaction = db.transaction((users) => {
for (const u of users) {
if (!u.name || !u.email || !u.password) {
results.errors.push({ email: u.email, error: 'Missing required fields' });
continue;
}
const exists = db.prepare('SELECT id FROM users WHERE email = ?').get(u.email);
if (exists) {
results.errors.push({ email: u.email, error: 'Email already exists' });
continue;
}
try {
const hash = bcrypt.hashSync(u.password, 10);
const r = insertUser.run(u.name, u.email, hash, u.role === 'admin' ? 'admin' : 'member');
addUserToPublicGroups(r.lastInsertRowid);
results.created.push(u.email);
} catch (e) {
results.errors.push({ email: u.email, error: e.message });
}
}
});
transaction(users);
res.json(results);
});
// Update user role (admin)
router.patch('/:id/role', authMiddleware, adminMiddleware, (req, res) => {
const { role } = req.body;
const db = getDb();
const target = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id);
if (!target) return res.status(404).json({ error: 'User not found' });
if (target.is_default_admin) return res.status(403).json({ error: 'Cannot modify default admin role' });
if (!['member', 'admin'].includes(role)) return res.status(400).json({ error: 'Invalid role' });
db.prepare("UPDATE users SET role = ?, updated_at = datetime('now') WHERE id = ?").run(role, target.id);
res.json({ success: true });
});
// Reset user password (admin)
router.patch('/:id/reset-password', authMiddleware, adminMiddleware, (req, res) => {
const { password } = req.body;
if (!password || password.length < 6) return res.status(400).json({ error: 'Password too short' });
const db = getDb();
const hash = bcrypt.hashSync(password, 10);
db.prepare("UPDATE users SET password = ?, must_change_password = 1, updated_at = datetime('now') WHERE id = ?").run(hash, req.params.id);
res.json({ success: true });
});
// Suspend user (admin)
router.patch('/:id/suspend', authMiddleware, adminMiddleware, (req, res) => {
const db = getDb();
const target = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id);
if (!target) return res.status(404).json({ error: 'User not found' });
if (target.is_default_admin) return res.status(403).json({ error: 'Cannot suspend default admin' });
db.prepare("UPDATE users SET status = 'suspended', updated_at = datetime('now') WHERE id = ?").run(target.id);
res.json({ success: true });
});
// Activate user (admin)
router.patch('/:id/activate', authMiddleware, adminMiddleware, (req, res) => {
const db = getDb();
db.prepare("UPDATE users SET status = 'active', updated_at = datetime('now') WHERE id = ?").run(req.params.id);
res.json({ success: true });
});
// Delete user (admin)
router.delete('/:id', authMiddleware, adminMiddleware, (req, res) => {
const db = getDb();
const target = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id);
if (!target) return res.status(404).json({ error: 'User not found' });
if (target.is_default_admin) return res.status(403).json({ error: 'Cannot delete default admin' });
db.prepare("UPDATE users SET status = 'deleted', updated_at = datetime('now') WHERE id = ?").run(target.id);
res.json({ success: true });
});
// Update own profile
router.patch('/me/profile', authMiddleware, (req, res) => {
const { displayName, aboutMe, hideAdminTag } = req.body;
const db = getDb();
db.prepare("UPDATE users SET display_name = ?, about_me = ?, hide_admin_tag = ?, updated_at = datetime('now') WHERE id = ?")
.run(displayName || null, aboutMe || null, hideAdminTag ? 1 : 0, req.user.id);
const user = db.prepare('SELECT id, name, email, role, status, avatar, about_me, display_name, hide_admin_tag FROM users WHERE id = ?').get(req.user.id);
res.json({ user });
});
// Upload avatar
router.post('/me/avatar', authMiddleware, uploadAvatar.single('avatar'), (req, res) => {
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
const avatarUrl = `/uploads/avatars/${req.file.filename}`;
const db = getDb();
db.prepare("UPDATE users SET avatar = ?, updated_at = datetime('now') WHERE id = ?").run(avatarUrl, req.user.id);
res.json({ avatarUrl });
});
module.exports = router;