feat: 初始化 kenaito-notebook 桌面记事本项目

This commit is contained in:
2026-06-13 21:08:00 +08:00
commit 5fafcd28e3
55 changed files with 9722 additions and 0 deletions
+338
View File
@@ -0,0 +1,338 @@
package handlers
import (
"log"
"net/http"
"net/mail"
"strings"
"github.com/gin-gonic/gin"
"kenaito-notebook-server/services"
)
func isValidEmail(email string) bool {
_, err := mail.ParseAddress(email)
return err == nil
}
type AuthHandler struct {
db *services.Database
auth *services.AuthService
email *services.EmailService
verify *services.VerificationService
}
func NewAuthHandler(db *services.Database, auth *services.AuthService, email *services.EmailService, verify *services.VerificationService) *AuthHandler {
return &AuthHandler{db: db, auth: auth, email: email, verify: verify}
}
type RegisterRequest struct {
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required"`
Code string `json:"code" binding:"required"`
}
type LoginRequest struct {
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required"`
}
type SendCodeRequest struct {
Email string `json:"email" binding:"required"`
Purpose string `json:"purpose" binding:"required"`
}
type VerifyCodeRequest struct {
Email string `json:"email" binding:"required"`
Code string `json:"code" binding:"required"`
Purpose string `json:"purpose" binding:"required"`
}
type ResetPasswordRequest struct {
Email string `json:"email" binding:"required"`
Code string `json:"code" binding:"required"`
NewPassword string `json:"newPassword" binding:"required"`
}
// SendCode 发送验证码
func (h *AuthHandler) SendCode(c *gin.Context) {
var req SendCodeRequest
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("[Auth] SendCode bind error: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写完整信息"})
return
}
if !isValidEmail(req.Email) {
log.Printf("[Auth] SendCode invalid email: %s", req.Email)
c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱格式不正确"})
return
}
if req.Purpose != "register" && req.Purpose != "reset" {
log.Printf("[Auth] SendCode invalid purpose: %s", req.Purpose)
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求类型"})
return
}
// 注册时检查邮箱是否已注册
if req.Purpose == "register" {
existing, _ := h.db.GetUserByEmail(req.Email)
if existing != nil {
log.Printf("[Auth] SendCode email already registered: %s", req.Email)
c.JSON(http.StatusConflict, gin.H{"error": "邮箱已被注册"})
return
}
}
// 重置密码时检查邮箱是否存在
if req.Purpose == "reset" {
existing, _ := h.db.GetUserByEmail(req.Email)
if existing == nil {
log.Printf("[Auth] SendCode email not found for reset: %s", req.Email)
c.JSON(http.StatusNotFound, gin.H{"error": "邮箱未注册"})
return
}
}
// 限频检查
if !h.verify.CheckRateLimit(req.Email, req.Purpose) {
log.Printf("[Auth] SendCode rate limit exceeded: %s, purpose: %s", req.Email, req.Purpose)
c.JSON(http.StatusTooManyRequests, gin.H{"error": "发送过于频繁,请 60 秒后重试"})
return
}
code := h.verify.GenerateCode(req.Email, req.Purpose)
if err := h.email.SendVerificationCode(req.Email, code, req.Purpose); err != nil {
log.Printf("[Auth] SendCode send email failed: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "发送验证码失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "验证码已发送"})
}
// VerifyCode 验证验证码
func (h *AuthHandler) VerifyCode(c *gin.Context) {
var req VerifyCodeRequest
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("[Auth] VerifyCode bind error: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写完整信息"})
return
}
if err := h.verify.VerifyCode(req.Email, req.Code, req.Purpose); err != nil {
log.Printf("[Auth] VerifyCode failed: email=%s, purpose=%s, err=%v", req.Email, req.Purpose, err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "验证码正确"})
}
// Register 用户注册
func (h *AuthHandler) Register(c *gin.Context) {
var req RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("[Auth] Register bind error: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写完整信息"})
return
}
if !isValidEmail(req.Email) {
log.Printf("[Auth] Register invalid email: %s", req.Email)
c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱格式不正确"})
return
}
// 检查邮箱是否已注册
existing, _ := h.db.GetUserByEmail(req.Email)
if existing != nil {
log.Printf("[Auth] Register email already registered: %s", req.Email)
c.JSON(http.StatusConflict, gin.H{"error": "邮箱已被注册"})
return
}
// 验证并消耗验证码
if err := h.verify.UseCode(req.Email, req.Code, "register"); err != nil {
log.Printf("[Auth] Register use code failed: email=%s, err=%v", req.Email, err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 哈希密码
hashedPassword, err := h.auth.HashPassword(req.Password)
if err != nil {
log.Printf("[Auth] Register hash password failed: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "注册失败"})
return
}
// 创建用户
user, err := h.db.CreateUser(req.Email, hashedPassword)
if err != nil {
log.Printf("[Auth] Register create user failed: email=%s, err=%v", req.Email, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "注册失败"})
return
}
// 生成 token
token, err := h.auth.GenerateToken(user.ID, user.Email)
if err != nil {
log.Printf("[Auth] Register generate token failed: userID=%d, err=%v", user.ID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "注册失败"})
return
}
c.JSON(http.StatusCreated, gin.H{
"token": token,
"user": user,
})
}
// Login 用户登录
func (h *AuthHandler) Login(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("[Auth] Login bind error: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写邮箱和密码"})
return
}
if !isValidEmail(req.Email) {
log.Printf("[Auth] Login invalid email: %s", req.Email)
c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱格式不正确"})
return
}
// 查找用户
user, err := h.db.GetUserByEmail(req.Email)
if err != nil {
log.Printf("[Auth] Login user not found: %s, err=%v", req.Email, err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "邮箱或密码错误"})
return
}
// 验证密码
if !h.auth.CheckPassword(req.Password, user.HashedPassword) {
log.Printf("[Auth] Login password mismatch: email=%s", req.Email)
c.JSON(http.StatusUnauthorized, gin.H{"error": "邮箱或密码错误"})
return
}
// 生成 token
token, err := h.auth.GenerateToken(user.ID, user.Email)
if err != nil {
log.Printf("[Auth] Login generate token failed: userID=%d, err=%v", user.ID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "登录失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
"user": user,
})
}
// ResetPassword 重置密码
func (h *AuthHandler) ResetPassword(c *gin.Context) {
var req ResetPasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("[Auth] ResetPassword bind error: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写完整信息"})
return
}
if !isValidEmail(req.Email) {
log.Printf("[Auth] ResetPassword invalid email: %s", req.Email)
c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱格式不正确"})
return
}
// 验证并消耗验证码
if err := h.verify.UseCode(req.Email, req.Code, "reset"); err != nil {
log.Printf("[Auth] ResetPassword use code failed: email=%s, err=%v", req.Email, err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 查找用户
user, err := h.db.GetUserByEmail(req.Email)
if err != nil {
log.Printf("[Auth] ResetPassword user not found: %s, err=%v", req.Email, err)
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
// 哈希新密码
hashedPassword, err := h.auth.HashPassword(req.NewPassword)
if err != nil {
log.Printf("[Auth] ResetPassword hash password failed: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "重置失败"})
return
}
// 更新密码
if err := h.db.UpdatePassword(user.ID, hashedPassword); err != nil {
log.Printf("[Auth] ResetPassword update password failed: userID=%d, err=%v", user.ID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "重置失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "密码重置成功"})
}
// GetMe 获取当前用户信息
func (h *AuthHandler) GetMe(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
log.Printf("[Auth] GetMe userID not found in context")
c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"})
return
}
user, err := h.db.GetUserByID(userID.(int64))
if err != nil {
log.Printf("[Auth] GetMe user not found: userID=%v, err=%v", userID, err)
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
c.JSON(http.StatusOK, user)
}
// AuthMiddleware JWT 认证中间件
func AuthMiddleware(auth *services.AuthService) gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
log.Printf("[Auth] Middleware missing Authorization header: ip=%s", c.ClientIP())
c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"})
c.Abort()
return
}
// 提取 Bearer token
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
log.Printf("[Auth] Middleware invalid auth format: ip=%s", c.ClientIP())
c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的认证格式"})
c.Abort()
return
}
payload, err := auth.ValidateToken(parts[1])
if err != nil {
log.Printf("[Auth] Middleware token validation failed: ip=%s, err=%v", c.ClientIP(), err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "登录已过期,请重新登录"})
c.Abort()
return
}
// 将用户信息存入上下文
c.Set("userID", payload.UserID)
c.Set("email", payload.Email)
c.Next()
}
}
+182
View File
@@ -0,0 +1,182 @@
package handlers
import (
"fmt"
"net/http"
"os"
"path/filepath"
"strconv"
"github.com/gin-gonic/gin"
"kenaito-notebook-server/models"
"kenaito-notebook-server/services"
)
type ExportHandler struct {
db *services.Database
exportSvc *services.ExportService
}
func NewExportHandler(db *services.Database, exportSvc *services.ExportService) *ExportHandler {
return &ExportHandler{db: db, exportSvc: exportSvc}
}
func (h *ExportHandler) ExportHTML(c *gin.Context) {
var req struct {
Title string `json:"title" binding:"required"`
Content string `json:"content" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
outputPath, err := h.getTempExportPath(req.Title, ".html")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := h.exportSvc.ExportHTML(req.Title, req.Content, outputPath); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"filePath": outputPath})
}
func (h *ExportHandler) ExportMarkdown(c *gin.Context) {
var req struct {
Title string `json:"title" binding:"required"`
Content string `json:"content" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
outputPath, err := h.getTempExportPath(req.Title, ".md")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := h.exportSvc.ExportMarkdown(req.Title, req.Content, outputPath); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"filePath": outputPath})
}
func (h *ExportHandler) ExportFolderZip(c *gin.Context) {
userID := c.GetInt64("userID")
folderID, err := strconv.ParseInt(c.Param("folderId"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid folder id"})
return
}
folderTree, err := h.db.GetFolderTree(userID, folderID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
noteInfoTree := convertNoteToNoteInfo(folderTree)
outputPath, err := h.getTempExportPath(folderTree.Title, ".zip")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := h.exportSvc.ExportFolderZip(folderTree.Title, noteInfoTree.Children, outputPath); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"filePath": outputPath})
}
func (h *ExportHandler) ExportAllZip(c *gin.Context) {
userID := c.GetInt64("userID")
allNotesTree, err := h.db.GetAllNotesTree(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
noteInfos := make([]*services.NoteInfo, len(allNotesTree))
for i, noteTree := range allNotesTree {
noteInfos[i] = convertNoteTreeToNoteInfo(noteTree)
}
outputPath, err := h.getTempExportPath("所有笔记", ".zip")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := h.exportSvc.ExportAllZip(noteInfos, outputPath); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"filePath": outputPath})
}
func (h *ExportHandler) getTempExportPath(title, ext string) (string, error) {
tmpDir := filepath.Join(os.TempDir(), "kenaito-notebook-export")
if err := os.MkdirAll(tmpDir, 0755); err != nil {
return "", fmt.Errorf("failed to create temp export dir: %w", err)
}
safeName := h.exportSvc.GetSafeFilename(title)
return filepath.Join(tmpDir, fmt.Sprintf("%s%s", safeName, ext)), nil
}
func convertNoteToNoteInfo(note *models.Note) *services.NoteInfo {
if note == nil {
return nil
}
info := &services.NoteInfo{
Title: note.Title,
Content: note.Content,
IsFolder: note.IsFolder,
}
if note.Children != nil && len(note.Children) > 0 {
info.Children = make([]*services.NoteInfo, len(note.Children))
for i, child := range note.Children {
info.Children[i] = convertNoteToNoteInfo(child)
}
}
return info
}
func convertNoteTreeToNoteInfo(noteTree *models.NoteTree) *services.NoteInfo {
if noteTree == nil {
return nil
}
info := &services.NoteInfo{
Title: noteTree.Title,
Content: noteTree.Content,
IsFolder: noteTree.IsFolder,
}
if noteTree.Children != nil && len(noteTree.Children) > 0 {
info.Children = make([]*services.NoteInfo, len(noteTree.Children))
for i, child := range noteTree.Children {
info.Children[i] = convertNoteTreeToNoteInfo(child)
}
}
return info
}
+60
View File
@@ -0,0 +1,60 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"kenaito-notebook-server/services"
)
type FileHandler struct {
fileSvc *services.FileService
}
func NewFileHandler(fileSvc *services.FileService) *FileHandler {
return &FileHandler{fileSvc: fileSvc}
}
func (h *FileHandler) SaveImage(c *gin.Context) {
var req struct {
Data string `json:"data" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
url, err := h.fileSvc.SaveImage(req.Data)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"url": url})
}
func (h *FileHandler) SaveFile(c *gin.Context) {
var req struct {
Data string `json:"data" binding:"required"`
Filename string `json:"filename" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
url, err := h.fileSvc.SaveFile(req.Data, req.Filename)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"url": url})
}
func (h *FileHandler) GetFileServerURL(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"url": h.fileSvc.GetBaseURL()})
}
func (h *FileHandler) GetAssetsPath(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"path": h.fileSvc.GetAssetsPath()})
}
+170
View File
@@ -0,0 +1,170 @@
package handlers
import (
"log"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"kenaito-notebook-server/services"
)
type NoteHandler struct {
db *services.Database
}
func NewNoteHandler(db *services.Database) *NoteHandler {
return &NoteHandler{db: db}
}
func (h *NoteHandler) GetNoteTree(c *gin.Context) {
userID := c.GetInt64("userID")
result, err := h.db.GetNoteTree(userID)
if err != nil {
log.Printf("[API] GetNoteTree error: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
}
func (h *NoteHandler) GetNote(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
note, err := h.db.GetNote(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, note)
}
func (h *NoteHandler) CreateNote(c *gin.Context) {
userID := c.GetInt64("userID")
var req struct {
ParentID int64 `json:"parentId"`
Title string `json:"title"`
IsFolder bool `json:"isFolder"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
note, err := h.db.CreateNote(userID, req.ParentID, req.Title, req.IsFolder)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, note)
}
func (h *NoteHandler) UpdateNote(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var req struct {
Title string `json:"title"`
Content string `json:"content"`
Tag string `json:"tag"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.db.UpdateNote(id, req.Title, req.Content, req.Tag); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "updated"})
}
func (h *NoteHandler) DeleteNote(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
if err := h.db.DeleteNote(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
func (h *NoteHandler) SearchNotes(c *gin.Context) {
userID := c.GetInt64("userID")
keyword := c.Query("q")
if keyword == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "keyword is required"})
return
}
notes, err := h.db.SearchNotes(userID, keyword)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, notes)
}
func (h *NoteHandler) GetTrashNotes(c *gin.Context) {
userID := c.GetInt64("userID")
notes, err := h.db.GetTrashNotes(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, notes)
}
func (h *NoteHandler) RestoreNote(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
if err := h.db.RestoreNote(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "restored"})
}
func (h *NoteHandler) PermanentDeleteNote(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
if err := h.db.PermanentDeleteNote(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "permanently deleted"})
}
func (h *NoteHandler) EmptyTrash(c *gin.Context) {
userID := c.GetInt64("userID")
if err := h.db.EmptyTrash(userID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "trash emptied"})
}