stud-manager / routes /auth.js
dvc890's picture
Update routes/auth.js
2d4f4e3 verified
const express = require('express');
const router = express.Router();
const { User, Student, ClassModel, NotificationModel, School } = require('../models');
// Helpers
const getQueryFilter = (req) => {
const s = req.headers['x-school-id'];
const role = req.headers['x-user-role'];
// 1. If requester is a Principal, they only see their own school's data
if (role === 'PRINCIPAL') {
if (!s) return { _id: null };
return { schoolId: s };
}
// 2. If requester is ADMIN (Super Admin)
if (role === 'ADMIN') {
// If filtering by a specific school (e.g. from dropdown), return that school's data
if (s) return { schoolId: s };
// If fetching "Global" list (no specific school selected or 'All' selected):
// We want to see:
// a) Users with NO schoolId (e.g. pending principals creating schools)
// b) Users from ALL schools (if we want a truly global list, we might remove the filter entirely,
// but usually 'getQueryFilter' is used to scope data.
// For the "User Management" page specifically, the route handler below handles the global flag.)
// Default behavior for other resources (like students/classes) when admin hasn't selected a school:
return {};
}
// 3. Teachers/Students
if (!s) return {};
return {
$or: [
{ schoolId: s },
{ schoolId: { $exists: false } },
{ schoolId: null }
]
};
};
const generateStudentNo = async () => {
const year = new Date().getFullYear();
const random = Math.floor(100000 + Math.random() * 900000);
return `${year}${random}`;
};
// --- Auth Routes ---
router.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await User.findOne({ username, password });
if (!user) return res.status(401).json({ message: 'Error' });
if (user.status !== 'active') return res.status(403).json({ error: 'PENDING_APPROVAL' });
res.json(user);
});
router.get('/me', async (req, res) => {
const username = req.headers['x-user-username'];
if (!username) return res.status(401).json({ error: 'Unauthorized' });
const user = await User.findOne({ username });
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
});
router.post('/register', async (req, res) => {
const { role, username, password, schoolId, trueName, seatNo, isCreatingSchool, newSchoolName, newSchoolCode } = req.body;
const className = req.body.className || req.body.homeroomClass;
try {
if (role === 'STUDENT') {
if (!trueName || !className) return res.status(400).json({ error: 'MISSING_FIELDS', message: '姓名和班级不能为空' });
const cleanName = trueName.trim(); const cleanClass = className.trim();
const existingProfile = await Student.findOne({ schoolId, name: { $regex: new RegExp(`^${cleanName}$`, 'i') }, className: cleanClass });
let finalUsername = '';
if (existingProfile) {
if (existingProfile.studentNo && existingProfile.studentNo.length > 5) finalUsername = existingProfile.studentNo;
else { finalUsername = await generateStudentNo(); existingProfile.studentNo = finalUsername; await existingProfile.save(); }
const userExists = await User.findOne({ username: finalUsername, schoolId });
if (userExists) return res.status(409).json({ error: userExists.status === 'active' ? 'ACCOUNT_EXISTS' : 'ACCOUNT_PENDING', message: '账号已存在' });
} else finalUsername = await generateStudentNo();
await User.create({ username: finalUsername, password, role: 'STUDENT', trueName: cleanName, schoolId, status: 'pending', homeroomClass: cleanClass, studentNo: finalUsername, seatNo: seatNo || '', parentName: req.body.parentName, parentPhone: req.body.parentPhone, address: req.body.address, idCard: req.body.idCard, gender: req.body.gender || 'Male', createTime: new Date() });
return res.json({ username: finalUsername });
}
// Check Username Duplication
const existing = await User.findOne({ username });
if (existing) return res.status(409).json({ error: 'USERNAME_EXISTS', message: '用户名已存在' });
// Handle Principal creating a school
let pendingSchoolData = null;
let finalSchoolId = schoolId;
if (role === 'PRINCIPAL' && isCreatingSchool) {
if (!newSchoolName || !newSchoolCode) return res.status(400).json({ error: 'MISSING_SCHOOL_INFO', message: '请输入学校名称和代码' });
// Check if school already exists
const schoolExists = await School.findOne({ $or: [{ name: newSchoolName }, { code: newSchoolCode }] });
if (schoolExists) return res.status(409).json({ error: 'SCHOOL_EXISTS', message: '该学校名称或代码已存在,请选择“加入学校”' });
pendingSchoolData = {
name: newSchoolName,
code: newSchoolCode
};
finalSchoolId = ''; // No school ID yet
}
await User.create({...req.body, schoolId: finalSchoolId, pendingSchoolData, status: 'pending', createTime: new Date()});
res.json({ username });
} catch(e) { res.status(500).json({ error: e.message }); }
});
router.post('/update-profile', async (req, res) => {
const { userId, trueName, phone, avatar, currentPassword, newPassword } = req.body;
try {
const user = await User.findById(userId);
if (!user) return res.status(404).json({ error: 'User not found' });
if (newPassword) { if (user.password !== currentPassword) return res.status(401).json({ error: 'INVALID_PASSWORD', message: '旧密码错误' }); user.password = newPassword; }
if (trueName) user.trueName = trueName; if (phone) user.phone = phone; if (avatar) user.avatar = avatar;
await user.save();
if (user.role === 'STUDENT') await Student.findOneAndUpdate({ studentNo: user.studentNo }, { name: user.trueName || user.username, phone: user.phone });
res.json({ success: true, user });
} catch (e) { res.status(500).json({ error: e.message }); }
});
// --- User Management Routes ---
router.get('/', async (req, res) => {
let filter = {};
// If requesting "Global" list (e.g. Admin Panel with 'global=true'), ignore school filter
if (req.query.global === 'true' && req.headers['x-user-role'] === 'ADMIN') {
filter = {}; // Return all users
} else {
// Otherwise apply standard school scoping
filter = getQueryFilter(req);
}
if (req.headers['x-user-role'] === 'PRINCIPAL') filter.role = { $ne: 'ADMIN' };
if (req.query.role) filter.role = req.query.role;
// Special case: If Admin is viewing a specific school, but we also want to see
// pending principals who HAVE NO schoolId yet (the "creating school" case),
// we need to OR the query.
// However, the UI usually separates "Global View" vs "School View".
// If Admin is in "Global View", they will see everything (including empty schoolId).
// If Admin selects a school, they only see that school's users.
// The issue was likely that `getQueryFilter` was being applied strictly even for global admin view.
// The change above (checking `req.query.global`) fixes this.
res.json(await User.find(filter).sort({ createTime: -1 }));
});
router.put('/:id', async (req, res) => {
const userId = req.params.id; const updates = req.body;
try {
const user = await User.findById(userId);
if (!user) return res.status(404).json({ error: 'User not found' });
// Handle Student activation logic (existing)
if (user.status !== 'active' && updates.status === 'active' && user.role === 'STUDENT') {
await Student.findOneAndUpdate({ studentNo: user.studentNo, schoolId: user.schoolId }, { $set: { schoolId: user.schoolId, studentNo: user.studentNo, seatNo: user.seatNo, name: user.trueName, className: user.homeroomClass, gender: user.gender || 'Male', parentName: user.parentName, parentPhone: user.parentPhone, address: user.address, idCard: user.idCard, status: 'Enrolled', birthday: '2015-01-01' } }, { upsert: true, new: true });
}
// NEW: Handle Principal School Creation on Activation
if (user.status !== 'active' && updates.status === 'active' && user.role === 'PRINCIPAL' && user.pendingSchoolData && user.pendingSchoolData.name) {
// Check again if school exists (double safety)
const exists = await School.findOne({ $or: [{ name: user.pendingSchoolData.name }, { code: user.pendingSchoolData.code }] });
if (exists) {
// Conflict! Just link to existing school? Or fail?
// Let's link to existing to allow approval, but clear creation data.
updates.schoolId = exists._id;
updates.pendingSchoolData = null;
} else {
// Create New School
const newSchool = await School.create({
name: user.pendingSchoolData.name,
code: user.pendingSchoolData.code
});
updates.schoolId = newSchool._id;
updates.pendingSchoolData = null;
}
}
await User.findByIdAndUpdate(userId, updates);
res.json({});
} catch (e) { res.status(500).json({ error: e.message }); }
});
router.delete('/:id', async (req, res) => {
const requesterRole = req.headers['x-user-role'];
if (requesterRole === 'PRINCIPAL') {
const user = await User.findById(req.params.id);
if (!user || user.schoolId !== req.headers['x-school-id']) return res.status(403).json({error: 'Permission denied'});
if (user.role === 'ADMIN') return res.status(403).json({error: 'Cannot delete admin'});
}
await User.findByIdAndDelete(req.params.id);
res.json({});
});
router.put('/:id/menu-order', async (req, res) => {
const { menuOrder } = req.body;
await User.findByIdAndUpdate(req.params.id, { menuOrder });
res.json({ success: true });
});
router.post('/class-application', async (req, res) => {
const { userId, type, targetClass, action } = req.body;
const userRole = req.headers['x-user-role']; const schoolId = req.headers['x-school-id'];
if (action === 'APPLY') { try { const user = await User.findById(userId); if(!user) return res.status(404).json({error:'User not found'}); await User.findByIdAndUpdate(userId, { classApplication: { type: type, targetClass: targetClass || '', status: 'PENDING' } }); await NotificationModel.create({ schoolId, targetRole: 'ADMIN', title: '新的班主任任免申请', content: `${user.trueName || user.username} 申请 ${type === 'CLAIM' ? '任教' : '卸任'},请及时处理。`, type: 'warning' }); return res.json({ success: true }); } catch (e) { return res.status(500).json({ error: e.message }); } }
if (userRole === 'ADMIN' || userRole === 'PRINCIPAL') {
const user = await User.findById(userId);
if (!user || !user.classApplication) return res.status(404).json({ error: 'Application not found' });
const appType = user.classApplication.type; const appTarget = user.classApplication.targetClass;
if (action === 'APPROVE') {
const updates = { classApplication: null }; const classes = await ClassModel.find({ schoolId });
if (appType === 'CLAIM') { updates.homeroomClass = appTarget; const matchedClass = classes.find(c => (c.grade + c.className) === appTarget); if (matchedClass) { const teacherIds = matchedClass.homeroomTeacherIds || []; if (!teacherIds.includes(userId)) { teacherIds.push(userId); const teachers = await User.find({ _id: { $in: teacherIds } }); const names = teachers.map(t => t.trueName || t.username).join(', '); await ClassModel.findByIdAndUpdate(matchedClass._id, { homeroomTeacherIds: teacherIds, teacherName: names }); } } } else if (appType === 'RESIGN') { updates.homeroomClass = ''; const matchedClass = classes.find(c => (c.grade + c.className) === user.homeroomClass); if (matchedClass) { const teacherIds = (matchedClass.homeroomTeacherIds || []).filter(id => id !== userId); const teachers = await User.find({ _id: { $in: teacherIds } }); const names = teachers.map(t => t.trueName || t.username).join(', '); await ClassModel.findByIdAndUpdate(matchedClass._id, { homeroomTeacherIds: teacherIds, teacherName: names }); } }
await User.findByIdAndUpdate(userId, updates);
} else await User.findByIdAndUpdate(userId, { 'classApplication.status': 'REJECTED' });
return res.json({ success: true });
}
res.status(403).json({ error: 'Permission denied' });
});
module.exports = router;