Files
note-manager/model/note.go
T
Your Name 74fa759274 feat: 云笔记增强 - 安全加固 + 回收站/版本历史/分享/批量导出 + 前端优化
- 安全: 认证改随机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
2026-08-11 11:21:29 +08:00

150 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"` // 父级目录 ID0 表示根目录
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"
}