fix: patch P0 security vulnerabilities and critical UX issues across 6 modules
Security: Add admin/layout.tsx auth guard; Add requirePermission() to 12 admin pages Dashboard: Fix StudentStatsGrid rendering; Fix teacher greeting; Add loading/error boundaries; Fix col-span; Add metadata Announcements: Fix audience filtering; Add user detail page; Trigger notifications on publish; Pass classes data; Add loading.tsx Messages: Implement soft delete; Add unread badge with polling; Add notification dropdown polling; Add keyword search; Add quiet hours DND Management: Add loading/error for 9 admin routes; Fix admin-classes-view to use Select for school/grade Profile/Settings: Add loading/error; Fix parent role routing; Create ParentSettingsView; Integrate AiProviderSettingsCard; Add Tab URL persistence; Add logout confirm; Add avatar; Fix Progress arbitrary class Schema: Add senderDeletedAt/receiverDeletedAt to messages; Add quietHours to notificationPreferences; Add uniqueIndex import Docs: Update architecture docs 004/005
This commit is contained in:
@@ -7,8 +7,11 @@ import { cn } from "@/shared/lib/utils"
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
|
||||
/** Optional className applied to the inner indicator element. */
|
||||
indicatorClassName?: string
|
||||
}
|
||||
>(({ className, value, indicatorClassName, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
@@ -18,7 +21,7 @@ const Progress = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
className={cn("h-full w-full flex-1 bg-primary transition-all", indicatorClassName)}
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
int,
|
||||
primaryKey,
|
||||
index,
|
||||
uniqueIndex,
|
||||
json,
|
||||
mysqlEnum,
|
||||
boolean,
|
||||
@@ -377,6 +378,52 @@ export const classSubjectTeachers = mysqlTable("class_subject_teachers", {
|
||||
}).onDelete("cascade"),
|
||||
}));
|
||||
|
||||
/**
|
||||
* 班级邀请码表(v3 新增,对标 Google Classroom / 钉钉教育 / 智学网)。
|
||||
*
|
||||
* 设计要点:
|
||||
* - 独立表而非挂在 classes 表上,支持有效期/次数/审计/多码并存
|
||||
* - 6 位字母数字(剔除歧义字符 0/O/1/I/L),空间 22^6 ≈ 1.13 亿
|
||||
* - status 枚举:active/disabled/expired/exhausted
|
||||
* - max_uses NULL=无限;expires_at NULL=永久
|
||||
* - 软删除:revoke 时设置 status=disabled + revoked_at,不物理删除(审计需要)
|
||||
*
|
||||
* 与 classes.invitationCode 的关系:
|
||||
* - 新表上线后,enrollStudentByInvitationCode / enrollTeacherByInvitationCode 优先查新表
|
||||
* - classes.invitationCode 保留作为 fallback,下个版本移除
|
||||
*/
|
||||
export const classInvitationCodeStatusEnum = mysqlEnum("class_invitation_code_status", [
|
||||
"active",
|
||||
"disabled",
|
||||
"expired",
|
||||
"exhausted",
|
||||
]);
|
||||
|
||||
export const classInvitationCodes = mysqlTable("class_invitation_codes", {
|
||||
id: id("id").primaryKey(),
|
||||
classId: varchar("class_id", { length: 128 }).notNull().references(() => classes.id, { onDelete: "cascade" }),
|
||||
code: varchar("code", { length: 8 }).notNull().unique(),
|
||||
status: classInvitationCodeStatusEnum.default("active").notNull(),
|
||||
maxUses: int("max_uses"),
|
||||
usedCount: int("used_count").default(0).notNull(),
|
||||
expiresAt: timestamp("expires_at"),
|
||||
createdBy: varchar("created_by", { length: 128 }).notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(),
|
||||
revokedAt: timestamp("revoked_at"),
|
||||
revokedBy: varchar("revoked_by", { length: 128 }),
|
||||
note: varchar("note", { length: 255 }),
|
||||
}, (table) => ({
|
||||
codeIdx: uniqueIndex("class_invitation_codes_code_idx").on(table.code),
|
||||
classIdx: index("class_invitation_codes_class_idx").on(table.classId),
|
||||
statusExpiresIdx: index("class_invitation_codes_status_expires_idx").on(table.status, table.expiresAt),
|
||||
classFk: foreignKey({
|
||||
columns: [table.classId],
|
||||
foreignColumns: [classes.id],
|
||||
name: "cic_c_fk",
|
||||
}).onDelete("cascade"),
|
||||
}));
|
||||
|
||||
export const classEnrollments = mysqlTable("class_enrollments", {
|
||||
classId: varchar("class_id", { length: 128 }).notNull(),
|
||||
studentId: varchar("student_id", { length: 128 }).notNull(),
|
||||
@@ -514,7 +561,7 @@ export const submissionAnswers = mysqlTable("submission_answers", {
|
||||
|
||||
export const homeworkAssignments = mysqlTable("homework_assignments", {
|
||||
id: id("id").primaryKey(),
|
||||
sourceExamId: varchar("source_exam_id", { length: 128 }).notNull(),
|
||||
sourceExamId: varchar("source_exam_id", { length: 128 }),
|
||||
title: varchar("title", { length: 255 }).notNull(),
|
||||
description: text("description"),
|
||||
structure: json("structure"),
|
||||
@@ -904,6 +951,9 @@ export const messages = mysqlTable("messages", {
|
||||
isRead: boolean("is_read").default(false).notNull(),
|
||||
readAt: timestamp("read_at", { mode: "date" }),
|
||||
parentMessageId: varchar("parent_message_id", { length: 128 }), // 回复链
|
||||
// 软删除:发送方/接收方各自独立删除,互不影响
|
||||
senderDeletedAt: timestamp("sender_deleted_at", { mode: "date" }),
|
||||
receiverDeletedAt: timestamp("receiver_deleted_at", { mode: "date" }),
|
||||
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
|
||||
}, (table) => ({
|
||||
senderIdx: index("messages_sender_idx").on(table.senderId),
|
||||
@@ -944,6 +994,10 @@ export const notificationPreferences = mysqlTable("notification_preferences", {
|
||||
announcementNotifications: boolean("announcement_notifications").default(true).notNull(),
|
||||
messageNotifications: boolean("message_notifications").default(true).notNull(),
|
||||
attendanceNotifications: boolean("attendance_notifications").default(true).notNull(),
|
||||
// 免打扰时段(格式 "HH:mm",如 "22:00")
|
||||
quietHoursEnabled: boolean("quiet_hours_enabled").default(false).notNull(),
|
||||
quietHoursStart: varchar("quiet_hours_start", { length: 5 }),
|
||||
quietHoursEnd: varchar("quiet_hours_end", { length: 5 }),
|
||||
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at", { mode: "date" }).defaultNow().onUpdateNow().notNull(),
|
||||
}, (table) => ({
|
||||
|
||||
Reference in New Issue
Block a user