## P1 功能(20 项) - 站内消息系统、家长仪表盘、学生考勤管理 - Excel 导入导出、用户批量导入、成绩导出 - 排课规则+自动排课+课表调整 - 成绩趋势+对比分析、密码安全策略、速率限制 - 数据变更日志、文件预览+存储策略、全文检索 - 依赖审计集成 CI、数据库定时备份、E2E 测试完善 - 通知偏好管理 ## 基础设施修复 - src/proxy.ts: 将 middleware 导出重命名为 proxy(Next.js 16 要求) - .env: MySQL 端口从 13002 切换至 14013 - scripts/create-db.ts: 新增数据库初始化脚本 ## 架构文档同步 - 004_architecture_impact_map.md 和 005_architecture_data.json 完整记录所有新增表、模块、路由、权限、依赖关系
120 lines
3.7 KiB
TypeScript
120 lines
3.7 KiB
TypeScript
/**
|
|
* Password security policy and account lockout helpers.
|
|
*
|
|
* These utilities are pure (no DB / I/O) so they can be safely used
|
|
* in both server and client contexts.
|
|
*/
|
|
|
|
export const PASSWORD_RULES = {
|
|
minLength: 8,
|
|
requireUppercase: true,
|
|
requireLowercase: true,
|
|
requireNumber: true,
|
|
requireSpecialChar: false,
|
|
maxLoginAttempts: 5,
|
|
lockoutDurationMinutes: 30,
|
|
} as const
|
|
|
|
export interface PasswordValidationResult {
|
|
valid: boolean
|
|
errors: string[]
|
|
}
|
|
|
|
/**
|
|
* Validate a password against the configured policy.
|
|
*/
|
|
export function validatePassword(password: string): PasswordValidationResult {
|
|
const errors: string[] = []
|
|
|
|
if (password.length < PASSWORD_RULES.minLength) {
|
|
errors.push(`Password must be at least ${PASSWORD_RULES.minLength} characters long`)
|
|
}
|
|
|
|
if (PASSWORD_RULES.requireUppercase && !/[A-Z]/.test(password)) {
|
|
errors.push("Password must contain at least one uppercase letter")
|
|
}
|
|
|
|
if (PASSWORD_RULES.requireLowercase && !/[a-z]/.test(password)) {
|
|
errors.push("Password must contain at least one lowercase letter")
|
|
}
|
|
|
|
if (PASSWORD_RULES.requireNumber && !/[0-9]/.test(password)) {
|
|
errors.push("Password must contain at least one number")
|
|
}
|
|
|
|
if (PASSWORD_RULES.requireSpecialChar && !/[^A-Za-z0-9]/.test(password)) {
|
|
errors.push("Password must contain at least one special character")
|
|
}
|
|
|
|
return { valid: errors.length === 0, errors }
|
|
}
|
|
|
|
export type PasswordStrength = "weak" | "medium" | "strong"
|
|
|
|
/**
|
|
* Compute a coarse password strength label based on length and character
|
|
* diversity. Useful for client-side strength indicators.
|
|
*/
|
|
export function getPasswordStrength(password: string): PasswordStrength {
|
|
if (password.length === 0) return "weak"
|
|
|
|
let score = 0
|
|
if (password.length >= 8) score++
|
|
if (password.length >= 12) score++
|
|
if (/[a-z]/.test(password)) score++
|
|
if (/[A-Z]/.test(password)) score++
|
|
if (/[0-9]/.test(password)) score++
|
|
if (/[^A-Za-z0-9]/.test(password)) score++
|
|
|
|
if (score <= 2) return "weak"
|
|
if (score <= 4) return "medium"
|
|
return "strong"
|
|
}
|
|
|
|
/**
|
|
* Determine whether an account should be considered locked given its
|
|
* failed-attempt count and the timestamp of the most recent failure.
|
|
*
|
|
* The lockout is lifted automatically once `lockoutDurationMinutes` have
|
|
* elapsed since `lastFailedAt`.
|
|
*/
|
|
export function isAccountLocked(
|
|
failedAttempts: number,
|
|
lastFailedAt: Date | null
|
|
): boolean {
|
|
if (failedAttempts < PASSWORD_RULES.maxLoginAttempts) return false
|
|
if (!lastFailedAt) return false
|
|
|
|
const lockoutMs = PASSWORD_RULES.lockoutDurationMinutes * 60 * 1000
|
|
const elapsed = Date.now() - lastFailedAt.getTime()
|
|
return elapsed < lockoutMs
|
|
}
|
|
|
|
/**
|
|
* Compute the remaining lockout time in milliseconds (0 if unlocked).
|
|
*/
|
|
export function getRemainingLockoutMs(
|
|
failedAttempts: number,
|
|
lastFailedAt: Date | null
|
|
): number {
|
|
if (!isAccountLocked(failedAttempts, lastFailedAt)) return 0
|
|
if (!lastFailedAt) return 0
|
|
const lockoutMs = PASSWORD_RULES.lockoutDurationMinutes * 60 * 1000
|
|
const elapsed = Date.now() - lastFailedAt.getTime()
|
|
return Math.max(0, lockoutMs - elapsed)
|
|
}
|
|
|
|
/**
|
|
* Human-readable summary of the password policy. Used by the UI to
|
|
* display requirements next to the password input.
|
|
*/
|
|
export const PASSWORD_REQUIREMENT_HINTS: string[] = [
|
|
`At least ${PASSWORD_RULES.minLength} characters`,
|
|
PASSWORD_RULES.requireUppercase ? "At least one uppercase letter (A-Z)" : null,
|
|
PASSWORD_RULES.requireLowercase ? "At least one lowercase letter (a-z)" : null,
|
|
PASSWORD_RULES.requireNumber ? "At least one number (0-9)" : null,
|
|
PASSWORD_RULES.requireSpecialChar
|
|
? "At least one special character (!@#$...)"
|
|
: null,
|
|
].filter((s): s is string => Boolean(s))
|