服务端:
- Class 新增 code(唯一)/passwordHash 与迁移(老数据回填 C00001 形式, 未设密码禁止登录)
- POST /auth/class-login: 班级码 + 班级密码 -> classSession 会话(sub=classId)
- PATCH /classes/:id/class-account: 管理员改班级码/重置密码(唯一性、格式校验)
- auth 中间件/socket 握手支持班级会话(sub 为 classId, 回查 Class 表, 加入 user:{classId} 房间)
- lib/session.ts: 班级设备代全班操作(通知已读/作业完成/成绩总览按全班读写), 普通学生账号行为不变
- 叫人消息同时广播到 user:{classId}, 班级设备立即上岛
- 班级列表/详情按权限返回班级码与密码状态(哈希不外泄)
客户端:
- 登录页改为班级码 + 班级密码(个人学生入口移除, /auth/login 接口保留兼容)
- 作业按钮/成绩标题按班级会话显示(全班标记完成 / 本班成绩)
- 冒烟脚本自动准备班级账号凭据(兼容全新与升级安装)
Web 管理端:
- 班级管理新增「班级账号」列与设置弹窗(改班级码/重置密码/随机生成)
- 新建班级可选自定义班级码, 默认由服务端生成
验证: verify:e2e 125/125(新增班级账号 18 项) verify:web 18/18 verify:desktop 40/40
(login=高一(1)班 classSession=true)
文档: README 新增「学生端班级账号」章节、API/迁移/验收/默认账号更新; 安装包 README 同步
87 lines
2.6 KiB
TypeScript
87 lines
2.6 KiB
TypeScript
import type { Request, RequestHandler } from 'express';
|
|
import type { UserRole } from '@classhelper/shared';
|
|
import { prisma } from '../lib/db.js';
|
|
import { ApiError } from '../lib/http.js';
|
|
import { extractBearerToken, verifyToken, type TokenPayload } from '../lib/jwt.js';
|
|
|
|
/**
|
|
* 认证中间件:解析 Bearer token 并回查数据库,
|
|
* 保证角色 / 班级变更(例如教师把学生转到别的班)立即生效,不被过期载荷影响。
|
|
*/
|
|
export function authenticate(): RequestHandler {
|
|
return async (req, _res, next) => {
|
|
try {
|
|
const token = extractBearerToken(req.headers.authorization);
|
|
if (!token) throw ApiError.unauthorized('缺少 Authorization 头');
|
|
|
|
const payload = verifyToken(token);
|
|
|
|
// 班级账号(班级设备):主体是班级而不是某个学生账号,回查 Class 表
|
|
if (payload.classSession) {
|
|
const record = await prisma.class.findUnique({
|
|
where: { id: payload.classId ?? payload.sub },
|
|
select: { id: true, name: true, code: true },
|
|
});
|
|
if (!record) throw ApiError.unauthorized('班级不存在或已被删除');
|
|
|
|
req.auth = {
|
|
token,
|
|
user: {
|
|
sub: record.id,
|
|
username: record.code,
|
|
name: record.name,
|
|
role: 'STUDENT',
|
|
classId: record.id,
|
|
classSession: true,
|
|
classCode: record.code,
|
|
},
|
|
};
|
|
next();
|
|
return;
|
|
}
|
|
|
|
const user = await prisma.user.findUnique({
|
|
where: { id: payload.sub },
|
|
select: { id: true, username: true, name: true, role: true, classId: true },
|
|
});
|
|
if (!user) throw ApiError.unauthorized('账号不存在或已被删除');
|
|
|
|
req.auth = {
|
|
token,
|
|
user: {
|
|
sub: user.id,
|
|
username: user.username,
|
|
name: user.name,
|
|
role: user.role as UserRole,
|
|
classId: user.classId,
|
|
},
|
|
};
|
|
next();
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
};
|
|
}
|
|
|
|
/** 角色守卫 */
|
|
export function requireRole(...roles: UserRole[]): RequestHandler {
|
|
return (req, _res, next) => {
|
|
const user = req.auth?.user;
|
|
if (!user) {
|
|
next(ApiError.unauthorized());
|
|
return;
|
|
}
|
|
if (!roles.includes(user.role)) {
|
|
next(ApiError.forbidden(`该操作仅限 ${roles.join(' / ')} 角色使用`));
|
|
return;
|
|
}
|
|
next();
|
|
};
|
|
}
|
|
|
|
/** 取出当前登录用户,未认证时抛 401 */
|
|
export function getAuthUser(req: Request): TokenPayload {
|
|
if (!req.auth?.user) throw ApiError.unauthorized();
|
|
return req.auth.user;
|
|
}
|