74fa759274
- 安全: 认证改随机token会话(弃固定cookie), 笔记密码SHA256升级为bcrypt(自动迁移), 堵住GET /api/notes/:id泄露带密码笔记, CORS收紧+SameSite防CSRF, 上传图片内容嗅探 - 回收站: 软删除(deleted_at), 列表/恢复/彻底删除/清空, 目录子树连删连恢复 - 版本历史: note_versions表存快照, 每次保存自动留档, 支持查看/回滚 - 分享: 生成随机token分享链接, 支持过期时间, 公开阅读页share.html - 批量导出: 全部笔记打包zip(按目录结构+front matter) - 前端: 深色模式, Mermaid图表, 待办清单checkbox, 字数统计; 后台新增回收站/历史/分享面板和批量导出按钮 - 新增deploy/note-manager.service systemd单元与smoke_test.py
150 lines
5.5 KiB
Go
150 lines
5.5 KiB
Go
package model
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"time"
|
||
|
||
"golang.org/x/crypto/bcrypt"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// bcryptCost 密码哈希成本因子
|
||
const bcryptCost = 10
|
||
|
||
// HashPassword 生成密码哈希(优先使用 bcrypt,向后兼容旧的 SHA-256)
|
||
// 返回值格式:new(bcrypt)哈希以 $2 开头;旧的 SHA-256 以 64 位十六进制开头。
|
||
func HashPassword(password string) string {
|
||
if password == "" {
|
||
return ""
|
||
}
|
||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
|
||
if err != nil {
|
||
// 极端情况下 fallback 到 sha256
|
||
s := sha256.Sum256([]byte(password))
|
||
return hex.EncodeToString(s[:])
|
||
}
|
||
return string(hash)
|
||
}
|
||
|
||
// IsBcryptHash 判断哈希是否为 bcrypt 格式
|
||
func IsBcryptHash(hash string) bool {
|
||
return len(hash) >= 4 && hash[:4] == "$2a$" || (len(hash) >= 4 && hash[:4] == "$2b$") || (len(hash) >= 4 && hash[:4] == "$2y$")
|
||
}
|
||
|
||
// CheckPassword 验证密码(兼容 bcrypt 与旧 SHA-256 哈希)
|
||
// 第二个返回值表示是否应当把存储的哈希升级为 bcrypt(旧 sha256 命中时返回 true)
|
||
func CheckPassword(password, hash string) (bool, bool) {
|
||
if hash == "" {
|
||
return true, false
|
||
}
|
||
if password == "" {
|
||
return false, false
|
||
}
|
||
if IsBcryptHash(hash) {
|
||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||
return err == nil, false
|
||
}
|
||
// 旧的 SHA-256 哈希
|
||
s := sha256.Sum256([]byte(password))
|
||
if hex.EncodeToString(s[:]) == hash {
|
||
// 命中旧哈希,建议升级为 bcrypt
|
||
return true, true
|
||
}
|
||
return false, false
|
||
}
|
||
|
||
// Note 笔记模型(也用于目录)
|
||
type Note struct {
|
||
ID uint `json:"id" gorm:"primaryKey"`
|
||
Title string `json:"title" gorm:"size:255;not null"`
|
||
Content string `json:"content" gorm:"type:text"`
|
||
Category string `json:"category" gorm:"size:100;index"`
|
||
Tags string `json:"tags" gorm:"type:text"` // JSON 数组格式存储
|
||
Password string `json:"-" gorm:"size:255"` // 访问密码(哈希存储)
|
||
IsPinned bool `json:"is_pinned" gorm:"default:false"`
|
||
IsFavorite bool `json:"is_favorite" gorm:"default:false"`
|
||
IsPublic bool `json:"is_public"` // 是否公开
|
||
ParentID uint `json:"parent_id" gorm:"default:0;index"` // 父级目录 ID,0 表示根目录
|
||
IsFolder bool `json:"is_folder" gorm:"default:false"` // 是否为文件夹
|
||
SortOrder int `json:"sort_order" gorm:"default:0"` // 排序顺序
|
||
ShareToken string `json:"-" gorm:"size:64;index"` // 分享令牌,空 = 未分享
|
||
ShareExpireAt *time.Time `json:"share_expire_at,omitempty"` // 分享过期时间,nil = 永久
|
||
VisitCount int `json:"visit_count" gorm:"default:0"` // 浏览次数
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // 软删除
|
||
}
|
||
|
||
// NoteListItem 笔记列表响应(不含内容和密码)
|
||
type NoteListItem struct {
|
||
ID uint `json:"id"`
|
||
Title string `json:"title"`
|
||
Category string `json:"category"`
|
||
Tags string `json:"tags"`
|
||
HasPassword bool `json:"has_password"` // 是否有密码保护
|
||
IsPinned bool `json:"is_pinned"`
|
||
IsFavorite bool `json:"is_favorite"`
|
||
IsPublic bool `json:"is_public"`
|
||
ParentID uint `json:"parent_id"`
|
||
IsFolder bool `json:"is_folder"`
|
||
SortOrder int `json:"sort_order"`
|
||
ShareToken string `json:"share_token,omitempty"` // 分享令牌(后台完整列表需要)
|
||
ShareExpireAt *time.Time `json:"share_expire_at,omitempty"`
|
||
VisitCount int `json:"visit_count"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// NoteCreateRequest 创建笔记请求
|
||
type NoteCreateRequest struct {
|
||
Title string `json:"title" binding:"required"`
|
||
Content string `json:"content"`
|
||
Category string `json:"category"`
|
||
Tags string `json:"tags"`
|
||
Password string `json:"password"`
|
||
IsPinned *bool `json:"is_pinned"`
|
||
IsFavorite *bool `json:"is_favorite"`
|
||
IsPublic *bool `json:"is_public"`
|
||
ParentID *uint `json:"parent_id"`
|
||
IsFolder bool `json:"is_folder"`
|
||
SortOrder int `json:"sort_order"`
|
||
}
|
||
|
||
// NoteUpdateRequest 更新笔记请求
|
||
type NoteUpdateRequest struct {
|
||
Title *string `json:"title"`
|
||
Content *string `json:"content"`
|
||
Category *string `json:"category"`
|
||
Tags *string `json:"tags"`
|
||
Password *string `json:"password"`
|
||
RemovePassword *bool `json:"remove_password"` // 是否移除密码
|
||
IsPinned *bool `json:"is_pinned"`
|
||
IsFavorite *bool `json:"is_favorite"`
|
||
IsPublic *bool `json:"is_public"`
|
||
ParentID *uint `json:"parent_id"`
|
||
IsFolder *bool `json:"is_folder"`
|
||
SortOrder *int `json:"sort_order"`
|
||
}
|
||
|
||
// NoteAccessRequest 笔记访问请求(验证密码)
|
||
type NoteAccessRequest struct {
|
||
Password string `json:"password"`
|
||
}
|
||
|
||
// NoteVersion 笔记版本历史
|
||
type NoteVersion struct {
|
||
ID uint `json:"id" gorm:"primaryKey"`
|
||
NoteID uint `json:"note_id" gorm:"index"`
|
||
Title string `json:"title"`
|
||
Content string `json:"content" gorm:"type:text"`
|
||
Category string `json:"category"`
|
||
Tags string `json:"tags"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
}
|
||
|
||
// TableName 指定版本表名
|
||
func (NoteVersion) TableName() string {
|
||
return "note_versions"
|
||
}
|