diff --git a/.gitignore b/.gitignore index 1ac1792..8929234 100644 --- a/.gitignore +++ b/.gitignore @@ -20,8 +20,8 @@ Thumbs.db logs/ *.log -# 配置文件(如有敏感信息) -# configs/*.yaml +# 配置文件(含敏感信息,仅本地保留) +configs/config.yaml # Go vendor/ diff --git a/cmd/server/user.go b/cmd/server/user.go index 9b434b0..20cd1a5 100644 --- a/cmd/server/user.go +++ b/cmd/server/user.go @@ -2,7 +2,10 @@ package main import ( "EnGin/internal/db" + "EnGin/internal/ent" + "EnGin/internal/ent/role" "EnGin/internal/global" + "EnGin/internal/utils/pwd" "bufio" "context" "fmt" @@ -35,17 +38,48 @@ var userCreateCmd = &cobra.Command{ fmt.Print("请输入密码: ") password, _ := reader.ReadString('\n') password = strings.TrimSpace(password) + hashedPwd, err := pwd.GenerateFromPassword(password) + if err != nil { + fmt.Errorf("密码加密错误 %w", err) + return + } if username == "" || password == "" { fmt.Println("用户名和密码不能为空") return } ctx := context.Background() - // 直接使用 db.Client 跨包调用并创建数据 + + // 1. 尝试查询 admin 角色 + adminRole, err := db.Client.Role.Query(). + Where(role.CodeEQ("admin")). + Only(ctx) + + if err != nil { + if ent.IsNotFound(err) { + // 2. 如果不存在 admin 角色,自动创建 + adminRole, err = db.Client.Role.Create(). + SetCode("admin"). + SetName("超级管理员"). + Save(ctx) + if err != nil { + global.Log.Error("创建管理员角色失败", zap.Error(err)) + fmt.Println("创建管理员角色失败: ", err.Error()) + return + } + fmt.Println("👉 系统中未发现 admin 角色,已自动创建。") + } else { + global.Log.Error("查询角色失败", zap.Error(err)) + fmt.Println("查询角色失败: ", err.Error()) + return + } + } + + // 3. 直接使用 db.Client 创建用户并挂载角色 u, err := db.Client.User.Create(). - // 根据您的 schema 字段做适当的设定,如: - // SetUsername(username). - // SetPassword(password). + SetUsername(username). + SetPassword(hashedPwd). + AddRoles(adminRole). // 💡 重点:挂载角色 Save(ctx) if err != nil { global.Log.Error("创建用户失败", zap.Error(err)) @@ -53,7 +87,7 @@ var userCreateCmd = &cobra.Command{ return } - fmt.Printf("✨ 用户创建成功!ID: %d, 用户名: %s\n", u.ID, username) + fmt.Printf("✨ 用户创建成功!ID: %d, 用户名: %s, 角色: admin\n", u.ID, username) }, } diff --git a/configs/config.yaml b/configs/config.yaml.dev similarity index 67% rename from configs/config.yaml rename to configs/config.yaml.dev index 5fdc3b8..aa98b7f 100644 --- a/configs/config.yaml +++ b/configs/config.yaml.dev @@ -1,9 +1,11 @@ -# EnGin 项目主配置文件 +# EnGin 项目开发环境配置模板 +# 使用方式:复制为 config.yaml 并填入真实值 +# cp configs/config.yaml.dev configs/config.yaml # Web 服务配置 server: port: 8080 # 服务监听的端口 - mode: "release" # 运行模式:debug (开发模式,带控制台彩色日志) / release (生产模式,只输出 JSON 日志) + mode: "debug" # 运行模式:debug (开发模式,带控制台彩色日志) / release (生产模式,只输出 JSON 日志) version: 2.0.4 allow_origins: - http://localhost:3000 @@ -12,9 +14,9 @@ database: driver: "postgres" # 驱动,支持 mysql 或 postgres host: "127.0.0.1" # 数据库主机地址 port: 5432 # 端口号:MySQL 默认 3306,PostgreSQL 默认 5432 - user: "unclesam" # 用户名 - password: "wangchan" # 密码 - dbname: "test_db" # 数据库名 + user: "your_user" # 用户名 + password: "your_password" # 密码 + dbname: "your_db" # 数据库名 # Zap 日志配置 logger: @@ -27,14 +29,14 @@ logger: compress: true # 是否对历史归档日志进行 gzip 压缩 jwt: - issuer: "山姆叔叔" - access_secret: "django-insecure-pfgn+96(rq74los_7d#n8#b07+$23vnxy*#y^y=eu9&_1eqk$7" - refresh_secret: "3676f2d0e99f294aab75997e16340370" + issuer: "your_issuer" + access_secret: "your-access-secret-here" + refresh_secret: "your-refresh-secret-here" access_expire: 2 # 小时 refresh_expire: 168 # 小时(7天) # Redis 缓存配置 redis: addr: "127.0.0.1:6379" # Redis 连接地址 - password: "wangchan5" # Redis 密码 (没有则为空) - db: 0 # 数据库编号 \ No newline at end of file + password: "" # Redis 密码 (没有则为空) + db: 0 # 数据库编号 diff --git a/internal/api/auth_api/enter.go b/internal/api/auth_api/enter.go new file mode 100644 index 0000000..fe1ca02 --- /dev/null +++ b/internal/api/auth_api/enter.go @@ -0,0 +1,4 @@ +package auth_api + +type AuthApi struct { +} diff --git a/internal/api/auth_api/login.go b/internal/api/auth_api/login.go new file mode 100644 index 0000000..000eddf --- /dev/null +++ b/internal/api/auth_api/login.go @@ -0,0 +1,74 @@ +package auth_api + +import ( + "EnGin/internal/global" + "EnGin/internal/middleware" + "EnGin/internal/service/auth_ser" + "EnGin/internal/utils/jwts" + "EnGin/internal/utils/res" + "errors" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type LoginRequest struct { + Username string `json:"username" binding:"required" label:"用户名"` + Password string `json:"password" binding:"required" label:"密码"` +} + +type LoginResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} + +func (AuthApi) LoginView(c *gin.Context) { + cr := middleware.GetBind[LoginRequest](c) + + u, err := auth_ser.Login(c.Request.Context(), cr.Username, cr.Password) + if err != nil { + if errors.Is(err, auth_ser.ErrUserNotFound) { + global.Log.Warn("用户不存在", zap.String("username", cr.Username), zap.Error(err)) + res.BadRequest("用户名或密码错误", c) + return + } + + if errors.Is(err, auth_ser.ErrAccount) { + res.FailWithMsg("用户名或密码错误", c) + return + } + + global.Log.Error("登陆系统异常", zap.Error(err)) + res.InternalError("登陆失败,请稍后重试", c) + return + } + + // 提取预加载的角色 ID 列表 + var roleIds []uint + for _, role := range u.Edges.Roles { + roleIds = append(roleIds, uint(role.ID)) + } + + claims := jwts.Claims{ + UserID: uint(u.ID), + Roles: roleIds, // 将关联查询出的角色 ID 塞进 token + Email: u.Email, + } + + accessToken, err := jwts.GenAccessToken(claims) + if err != nil { + res.InternalError("生成 accessToken 失败", c) + return + } + + refreshToken, err := jwts.GenRefreshToken(uint(u.ID)) + if err != nil { + res.InternalError("生成 refreshToken 失败", c) + return + } + + res.OkWithData(LoginResponse{ + AccessToken: accessToken, + RefreshToken: refreshToken, + }, c) +} diff --git a/internal/api/enter.go b/internal/api/enter.go index 7c70e1d..1391948 100644 --- a/internal/api/enter.go +++ b/internal/api/enter.go @@ -1,6 +1,9 @@ package api +import "EnGin/internal/api/auth_api" + type Api struct { + AuthApi auth_api.AuthApi } // App 统一导出全局 API 实例 diff --git a/internal/global/errors.go b/internal/global/errors.go new file mode 100644 index 0000000..6a89266 --- /dev/null +++ b/internal/global/errors.go @@ -0,0 +1,11 @@ +package global + +import "errors" + +// 预定义全局通用的系统级/数据库级错误 +var ( + ErrDbQuery = errors.New("数据库查询异常") + ErrDbSave = errors.New("数据保存失败") + ErrDbDelete = errors.New("数据删除失败") + ErrDbUpdate = errors.New("数据更新失败") +) diff --git a/internal/router/auth_router.go b/internal/router/auth_router.go index 38651cd..6464c49 100644 --- a/internal/router/auth_router.go +++ b/internal/router/auth_router.go @@ -1,7 +1,18 @@ package router -import "github.com/gin-gonic/gin" +import ( + "EnGin/internal/api" + "EnGin/internal/api/auth_api" + "EnGin/internal/middleware" + + "github.com/gin-gonic/gin" +) func AuthRouter(g *gin.RouterGroup) { + app := api.App.AuthApi + g.POST("login", + middleware.BindJsonMiddleware[auth_api.LoginRequest], + middleware.AuthRateLimit, + app.LoginView) } diff --git a/internal/service/auth_ser/login.go b/internal/service/auth_ser/login.go new file mode 100644 index 0000000..7f4c725 --- /dev/null +++ b/internal/service/auth_ser/login.go @@ -0,0 +1,33 @@ +package auth_ser + +import ( + "EnGin/internal/db" + "EnGin/internal/ent" + "EnGin/internal/ent/user" + "EnGin/internal/global" + "EnGin/internal/utils/pwd" + "context" + "errors" +) + +var ( + ErrUserNotFound = errors.New("用户不存在") + ErrAccount = errors.New("账号和密码错误") +) + +func Login(ctx context.Context, username, password string) (*ent.User, error) { + u, err := db.Client.User.Query().Where(user.UsernameEQ(username)).WithRoles().Only(ctx) + if err != nil { + if ent.IsNotFound(err) { + return nil, ErrUserNotFound + } + + return nil, global.ErrDbQuery + } + + if !pwd.CompareHashAndPassword(u.Password, password) { + return nil, ErrAccount + } + + return u, nil +} diff --git a/internal/utils/pwd/enter.go b/internal/utils/pwd/enter.go new file mode 100644 index 0000000..0651bea --- /dev/null +++ b/internal/utils/pwd/enter.go @@ -0,0 +1,20 @@ +package pwd + +import ( + "go.uber.org/zap" + "golang.org/x/crypto/bcrypt" +) + +func GenerateFromPassword(password string) (string, error) { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + zap.L().Error("密码加密失败", zap.Error(err)) + return "", err + } + return string(hashedPassword), nil +} + +func CompareHashAndPassword(hashedPassword string, password string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) + return err == nil +}