feat: 添加用户注册登录功能,实现数据和配置的用户隔离

- 新增 users 和 user_settings 表,repositories/analyses/review_notes 添加 user_id 列
- 实现基于邮箱+密码的注册登录,密码使用 bcrypt 哈希
- 使用 gin-contrib/sessions cookie-based session 管理
- 所有仓库、分析记录、review notes 按用户隔离
- 用户设置(LLM 配置、review 参数、缓存配置)独立存储
- 新增登录/注册页面,导航栏显示用户邮箱和退出按钮
- 前端 fetch 请求统一添加 credentials: 'same-origin'
- 支持 SESSION_SECRET 环境变量配置会话密钥
This commit is contained in:
2026-06-20 21:57:46 +08:00
parent 953c83d9aa
commit 9c843b79ba
28 changed files with 837 additions and 176 deletions
+44 -22
View File
@@ -13,6 +13,8 @@ import (
"syscall"
"time"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"github.com/HoHD/PR-Helper/config"
@@ -32,6 +34,15 @@ func main() {
gin.SetMode(cfg.GinMode)
r := gin.Default()
// Session middleware
store := cookie.NewStore([]byte(cfg.SessionSecret))
store.Options(sessions.Options{
MaxAge: 86400 * 7, // 7 days
Path: "/",
HttpOnly: true,
})
r.Use(sessions.Sessions("pr_session", store))
// Custom template functions
funcMap := template.FuncMap{
"add": func(a, b int) int { return a + b },
@@ -72,37 +83,48 @@ func main() {
r.Static("/static", "./static")
// Handlers
auth := handlers.NewAuthHandler(db.Conn())
pages := handlers.NewPageHandler(db.Conn())
settings := handlers.NewSettingsHandler(db.Conn())
repos := handlers.NewReposHandler(db.Conn(), cfg.ReposDir())
generate := handlers.NewGenerateHandler(db.Conn())
review := handlers.NewReviewHandler(db.Conn())
// Public routes (no auth required)
r.GET("/login", auth.Login)
r.GET("/register", auth.Register)
r.POST("/api/auth/login", auth.HandleLogin)
r.POST("/api/auth/register", auth.HandleRegister)
r.POST("/api/auth/logout", auth.HandleLogout)
// Protected routes (auth required)
authMw := handlers.AuthRequired(db.Conn())
// Page routes
r.GET("/", pages.Index)
r.GET("/repo/:id", pages.Repo)
r.GET("/repo/:id/generate", pages.Generate)
r.GET("/repo/:id/review", pages.Review)
r.GET("/settings", pages.Settings)
r.GET("/", authMw, pages.Index)
r.GET("/repo/:id", authMw, pages.Repo)
r.GET("/repo/:id/generate", authMw, pages.Generate)
r.GET("/repo/:id/review", authMw, pages.Review)
r.GET("/settings", authMw, pages.Settings)
// API routes
r.GET("/api/settings", settings.GetSettings)
r.PUT("/api/settings", settings.UpdateSettings)
r.GET("/api/repos", repos.ListRepos)
r.POST("/api/repos", repos.CloneRepo)
r.DELETE("/api/repos/:id", repos.DeleteRepo)
r.POST("/api/repos/:id/cleanup", repos.CleanupRepos)
r.GET("/api/repos/:id/graph", repos.GetGraph)
r.GET("/api/repos/:id/refs", repos.GetRefs)
r.GET("/api/repos/:id/commits", repos.GetCommits)
r.GET("/api/repos/:id/diff", repos.GetDiff)
r.POST("/api/repos/:id/generate", generate.Generate)
r.POST("/api/repos/:id/review", review.Review)
r.GET("/api/repos/:id/review/analyses", review.ListReviews)
r.GET("/api/repos/:id/review/analyses/:aid", review.GetReview)
r.POST("/api/repos/:id/review/notes", review.SaveNotes)
r.GET("/api/repos/:id/review/notes", review.GetNotes)
r.POST("/api/repos/:id/review/pdf", review.GeneratePDF)
r.GET("/api/settings", authMw, settings.GetSettings)
r.PUT("/api/settings", authMw, settings.UpdateSettings)
r.GET("/api/repos", authMw, repos.ListRepos)
r.POST("/api/repos", authMw, repos.CloneRepo)
r.DELETE("/api/repos/:id", authMw, repos.DeleteRepo)
r.POST("/api/repos/:id/cleanup", authMw, repos.CleanupRepos)
r.GET("/api/repos/:id/graph", authMw, repos.GetGraph)
r.GET("/api/repos/:id/refs", authMw, repos.GetRefs)
r.GET("/api/repos/:id/commits", authMw, repos.GetCommits)
r.GET("/api/repos/:id/diff", authMw, repos.GetDiff)
r.POST("/api/repos/:id/generate", authMw, generate.Generate)
r.POST("/api/repos/:id/review", authMw, review.Review)
r.GET("/api/repos/:id/review/analyses", authMw, review.ListReviews)
r.GET("/api/repos/:id/review/analyses/:aid", authMw, review.GetReview)
r.POST("/api/repos/:id/review/notes", authMw, review.SaveNotes)
r.GET("/api/repos/:id/review/notes", authMw, review.GetNotes)
r.POST("/api/repos/:id/review/pdf", authMw, review.GeneratePDF)
// Graceful shutdown with signal handling
srv := &http.Server{