feat: 初始化配置中心项目
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig
|
||||
Database DatabaseConfig
|
||||
JWT JWTConfig
|
||||
Crypto CryptoConfig
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Port int
|
||||
Mode string
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Name string
|
||||
User string
|
||||
Password string
|
||||
}
|
||||
|
||||
type JWTConfig struct {
|
||||
Secret string
|
||||
ExpireHours int
|
||||
}
|
||||
|
||||
type CryptoConfig struct {
|
||||
SecretKey string
|
||||
}
|
||||
|
||||
var Cfg *Config
|
||||
|
||||
func Load(path string) error {
|
||||
baseProps, err := parseProps(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
active := baseProps["active"]
|
||||
if active == "" {
|
||||
return fmt.Errorf("missing required config: active")
|
||||
}
|
||||
|
||||
dir := filepath.Dir(path)
|
||||
profilePath := filepath.Join(dir, "application-"+active+".properties")
|
||||
profileProps, err := parseProps(profilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load profile %s: %w", active, err)
|
||||
}
|
||||
|
||||
for k, v := range profileProps {
|
||||
baseProps[k] = v
|
||||
}
|
||||
|
||||
Cfg = &Config{}
|
||||
|
||||
Cfg.Server.Port = intProp(baseProps, "server.port", 8080)
|
||||
setServerMode(active)
|
||||
|
||||
Cfg.Database.Host = baseProps["database.host"]
|
||||
Cfg.Database.Port = intProp(baseProps, "database.port", 3306)
|
||||
Cfg.Database.Name = baseProps["database.name"]
|
||||
Cfg.Database.User = baseProps["database.user"]
|
||||
Cfg.Database.Password = baseProps["database.password"]
|
||||
|
||||
Cfg.JWT.Secret = baseProps["jwt.secret"]
|
||||
Cfg.JWT.ExpireHours = intProp(baseProps, "jwt.expireHours", 24)
|
||||
|
||||
Cfg.Crypto.SecretKey = baseProps["crypto.secretKey"]
|
||||
|
||||
if Cfg.Database.Host == "" {
|
||||
return fmt.Errorf("missing required config: database.host")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setServerMode(active string) {
|
||||
if "daily" == active {
|
||||
Cfg.Server.Mode = gin.DebugMode
|
||||
} else if "stage" == active || "online" == active {
|
||||
Cfg.Server.Mode = gin.ReleaseMode
|
||||
} else {
|
||||
Cfg.Server.Mode = gin.DebugMode
|
||||
}
|
||||
}
|
||||
|
||||
func parseProps(path string) (map[string]string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
props := make(map[string]string)
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
idx := strings.Index(line, "=")
|
||||
if idx < 0 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(line[:idx])
|
||||
val := strings.TrimSpace(line[idx+1:])
|
||||
props[key] = val
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return props, nil
|
||||
}
|
||||
|
||||
func intProp(props map[string]string, key string, defaultVal int) int {
|
||||
v, ok := props[key]
|
||||
if !ok || v == "" {
|
||||
return defaultVal
|
||||
}
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return defaultVal
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (d *DatabaseConfig) DSN() string {
|
||||
return d.User + ":" + d.Password + "@tcp(" + d.Host + ":" + strconv.Itoa(d.Port) + ")/" + d.Name + "?charset=utf8&parseTime=true"
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"kenaito-conf/server/internal/config"
|
||||
"kenaito-conf/server/internal/model"
|
||||
"kenaito-conf/server/internal/util"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
var Engine *xorm.Engine
|
||||
|
||||
func Init() error {
|
||||
cfg := config.Cfg.Database
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=true&loc=Local",
|
||||
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Name)
|
||||
|
||||
var err error
|
||||
Engine, err = xorm.NewEngine("mysql", dsn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
Engine.SetMaxOpenConns(20)
|
||||
Engine.SetMaxIdleConns(5)
|
||||
|
||||
// sync table structures
|
||||
if err := Engine.Sync2(
|
||||
new(model.BusinessGroup),
|
||||
new(model.Profile),
|
||||
new(model.Application),
|
||||
new(model.AppExtrAttr),
|
||||
new(model.AppConfig),
|
||||
new(model.AppConfigHistory),
|
||||
new(model.User),
|
||||
new(model.UserPermission),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// insert initial data if tables are empty
|
||||
return initSeedData()
|
||||
}
|
||||
|
||||
func initSeedData() error {
|
||||
// check if profiles table is empty
|
||||
count, err := Engine.Count(new(model.Profile))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// insert profiles
|
||||
profiles := []model.Profile{
|
||||
{Name: "daily", Alias: "测试环境", IsDefault: true, Enabled: true},
|
||||
{Name: "stage", Alias: "预发布环境", IsDefault: true, Enabled: true},
|
||||
{Name: "online", Alias: "线上环境", IsDefault: true, Enabled: true},
|
||||
}
|
||||
for _, p := range profiles {
|
||||
if _, err := Engine.Insert(&p); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// insert admin user
|
||||
now := time.Now()
|
||||
hashedPassword, err := util.HashPassword("admin123")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
admin := model.User{
|
||||
Name: "admin",
|
||||
Password: hashedPassword,
|
||||
Type: "superAdmin",
|
||||
Enabled: true,
|
||||
CreatedAt: &now,
|
||||
}
|
||||
if _, err := Engine.Insert(&admin); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// insert default business group
|
||||
group := model.BusinessGroup{
|
||||
Name: "默认组",
|
||||
Enabled: true,
|
||||
CreatedAt: &now,
|
||||
CreatedBy: "admin",
|
||||
}
|
||||
if _, err := Engine.Insert(&group); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"kenaito-conf/server/internal/dao"
|
||||
"kenaito-conf/server/internal/middleware"
|
||||
"kenaito-conf/server/internal/model"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AppHandler struct{}
|
||||
|
||||
func NewAppHandler() *AppHandler {
|
||||
return &AppHandler{}
|
||||
}
|
||||
|
||||
type AppListRequest struct {
|
||||
PageNo int `form:"pageNo" binding:"required"`
|
||||
PageSize int `form:"pageSize" binding:"required"`
|
||||
AppType string `form:"appType"`
|
||||
}
|
||||
|
||||
func (h *AppHandler) List(c *gin.Context) {
|
||||
var req AppListRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.WrapperResponse{Code: 400, Msg: "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
query := dao.Engine.Where("enabled = 1")
|
||||
|
||||
if !userInfo.IsSuperAdmin() {
|
||||
if userInfo.IsGroupAdmin() && userInfo.GroupId != nil {
|
||||
query = query.And("group_id = ?", *userInfo.GroupId)
|
||||
} else {
|
||||
userId, _ := strconv.Atoi(userInfo.Id)
|
||||
query = query.And("(owner_id = ? OR id IN (SELECT app_id FROM user_permissions WHERE user_id = ?))", userId, userId)
|
||||
}
|
||||
}
|
||||
|
||||
var apps []model.Application
|
||||
total, _ := query.Count(&model.Application{})
|
||||
query.Limit(req.PageSize, (req.PageNo-1)*req.PageSize).Find(&apps)
|
||||
|
||||
// load group names
|
||||
groupIds := make(map[int]bool)
|
||||
for _, app := range apps {
|
||||
if app.GroupId != nil {
|
||||
groupIds[*app.GroupId] = true
|
||||
}
|
||||
}
|
||||
groupMap := make(map[int]string)
|
||||
for gid := range groupIds {
|
||||
var group model.BusinessGroup
|
||||
if has, _ := dao.Engine.ID(gid).Get(&group); has {
|
||||
groupMap[gid] = group.Name
|
||||
}
|
||||
}
|
||||
for i := range apps {
|
||||
if apps[i].GroupId != nil {
|
||||
apps[i].GroupName = groupMap[*apps[i].GroupId]
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.PageResult{PageNo: req.PageNo, PageSize: req.PageSize, Total: total, Data: apps})
|
||||
}
|
||||
|
||||
func (h *AppHandler) GetById(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var app model.Application
|
||||
has, _ := dao.Engine.ID(id).Get(&app)
|
||||
if !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 404, Msg: "应用不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: app})
|
||||
}
|
||||
|
||||
func (h *AppHandler) Tree(c *gin.Context) {
|
||||
var apps []model.Application
|
||||
dao.Engine.Find(&apps)
|
||||
|
||||
tree := make(map[string]*AppTreeModel)
|
||||
for _, app := range apps {
|
||||
node := &AppTreeModel{
|
||||
Id: strconv.Itoa(app.Id),
|
||||
Name: app.Name,
|
||||
Code: app.Code,
|
||||
ServiceId: app.ServiceId,
|
||||
Children: make([]*AppTreeModel, 0),
|
||||
}
|
||||
tree[strconv.Itoa(app.Id)] = node
|
||||
}
|
||||
|
||||
result := make([]*AppTreeModel, 0)
|
||||
for _, app := range apps {
|
||||
node := tree[strconv.Itoa(app.Id)]
|
||||
if app.ParentId == nil {
|
||||
result = append(result, node)
|
||||
} else {
|
||||
parentId := strconv.Itoa(*app.ParentId)
|
||||
if parent, ok := tree[parentId]; ok {
|
||||
parent.Children = append(parent.Children, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: result})
|
||||
}
|
||||
|
||||
type AddAppRequest struct {
|
||||
Id int `json:"id"`
|
||||
GroupId *int `json:"groupId"`
|
||||
ParentId *int `json:"parentId"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Code string `json:"code" binding:"required"`
|
||||
ServiceId string `json:"serviceId"`
|
||||
OwnerId *int `json:"ownerId"`
|
||||
Remarks string `json:"remarks"`
|
||||
}
|
||||
|
||||
func (h *AppHandler) Add(c *gin.Context) {
|
||||
var req AddAppRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.WrapperResponse{Code: 400, Msg: "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
|
||||
// check if code exists
|
||||
var existing model.Application
|
||||
if has, _ := dao.Engine.Where("code = ?", req.Code).Get(&existing); has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 1002, Msg: "应用已存在"})
|
||||
return
|
||||
}
|
||||
|
||||
ownerId := req.OwnerId
|
||||
if ownerId == nil {
|
||||
uid, _ := strconv.Atoi(userInfo.Id)
|
||||
ownerId = &uid
|
||||
}
|
||||
|
||||
var owner model.User
|
||||
if has, _ := dao.Engine.ID(*ownerId).Get(&owner); !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: "负责人不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
app := model.Application{
|
||||
GroupId: req.GroupId,
|
||||
ParentId: req.ParentId,
|
||||
Name: req.Name,
|
||||
Code: req.Code,
|
||||
ServiceId: req.ServiceId,
|
||||
OwnerId: ownerId,
|
||||
OwnerName: owner.Name,
|
||||
Remarks: req.Remarks,
|
||||
Enabled: true,
|
||||
CreatedAt: func() *time.Time { t := time.Now(); return &t }(),
|
||||
CreatedBy: userInfo.Name,
|
||||
}
|
||||
|
||||
if req.ParentId != nil {
|
||||
var parent model.Application
|
||||
if has, _ := dao.Engine.ID(*req.ParentId).Get(&parent); has {
|
||||
app.GroupId = parent.GroupId
|
||||
app.IsModule = true
|
||||
app.Code = parent.Code + "-" + req.Code
|
||||
}
|
||||
}
|
||||
|
||||
dao.Engine.Insert(&app)
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: true})
|
||||
}
|
||||
|
||||
func (h *AppHandler) Update(c *gin.Context) {
|
||||
var req AddAppRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.WrapperResponse{Code: 400, Msg: "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Id == 0 {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: "id参数缺失"})
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
has, _ := dao.Engine.ID(req.Id).Get(&app)
|
||||
if !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 404, Msg: "应用不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
|
||||
app.Name = req.Name
|
||||
app.ServiceId = req.ServiceId
|
||||
app.Remarks = req.Remarks
|
||||
if req.OwnerId != nil {
|
||||
if app.OwnerId == nil || *req.OwnerId != *app.OwnerId {
|
||||
var owner model.User
|
||||
if has, _ := dao.Engine.ID(*req.OwnerId).Get(&owner); has {
|
||||
app.OwnerName = owner.Name
|
||||
}
|
||||
}
|
||||
app.OwnerId = req.OwnerId
|
||||
}
|
||||
app.UpdatedBy = userInfo.Name
|
||||
now := time.Now()
|
||||
app.UpdatedAt = &now
|
||||
|
||||
dao.Engine.ID(app.Id).Cols("name", "service_id", "remarks", "owner_id", "owner_name", "updated_by", "updated_at").Update(&app)
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: true})
|
||||
}
|
||||
|
||||
func (h *AppHandler) Delete(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
|
||||
if !userInfo.IsSuperAdmin() {
|
||||
c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: "超级管理员才能删除"})
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
has, _ := dao.Engine.ID(id).Get(&app)
|
||||
if !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 404, Msg: "应用不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
app.Enabled = false
|
||||
app.UpdatedBy = userInfo.Name
|
||||
now := time.Now()
|
||||
app.UpdatedAt = &now
|
||||
dao.Engine.ID(app.Id).Cols("enabled", "updated_by", "updated_at").Update(&app)
|
||||
|
||||
// soft delete related configs
|
||||
dao.Engine.Where("app_id = ?", id).Cols("enabled").Update(&model.AppConfig{Enabled: false})
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0})
|
||||
}
|
||||
|
||||
func (h *AppHandler) Switch(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
|
||||
var app model.Application
|
||||
has, _ := dao.Engine.ID(id).Get(&app)
|
||||
if !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 404, Msg: "应用不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
app.Enabled = !app.Enabled
|
||||
app.UpdatedBy = userInfo.Name
|
||||
now := time.Now()
|
||||
app.UpdatedAt = &now
|
||||
dao.Engine.ID(app.Id).Cols("enabled", "updated_by", "updated_at").Update(&app)
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: true})
|
||||
}
|
||||
|
||||
func (h *AppHandler) Options(c *gin.Context) {
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
query := dao.Engine.Where("enabled = 1")
|
||||
|
||||
if !userInfo.IsSuperAdmin() {
|
||||
if userInfo.IsGroupAdmin() && userInfo.GroupId != nil {
|
||||
query = query.And("group_id = ?", *userInfo.GroupId)
|
||||
} else {
|
||||
userId, _ := strconv.Atoi(userInfo.Id)
|
||||
query = query.And("owner_id = ?", userId)
|
||||
}
|
||||
}
|
||||
|
||||
var apps []model.Application
|
||||
query.Find(&apps)
|
||||
|
||||
options := make([]model.SelectOption, 0)
|
||||
for _, app := range apps {
|
||||
options = append(options, model.SelectOption{Value: strconv.Itoa(app.Id), Label: app.Name})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: options})
|
||||
}
|
||||
|
||||
type AppTreeModel struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Code string `json:"code"`
|
||||
ServiceId string `json:"serviceId,omitempty"`
|
||||
Children []*AppTreeModel `json:"children"`
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"kenaito-conf/server/internal/middleware"
|
||||
"kenaito-conf/server/internal/model"
|
||||
"kenaito-conf/server/internal/service"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
authService *service.AuthService
|
||||
}
|
||||
|
||||
func NewAuthHandler(authService *service.AuthService) *AuthHandler {
|
||||
return &AuthHandler{authService: authService}
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
UserName string `json:"userName" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Login(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.WrapperResponse{Code: 400, Msg: "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
token, userInfo, err := h.authService.Login(req.UserName, req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{
|
||||
Code: 0,
|
||||
Data: gin.H{"token": token, "userInfo": userInfo},
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) GetLoginUserInfo(c *gin.Context) {
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
if userInfo == nil {
|
||||
c.JSON(http.StatusUnauthorized, model.WrapperResponse{Code: 401, Msg: "未登录"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: userInfo})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Logout(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0})
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"kenaito-conf/server/internal/dao"
|
||||
"kenaito-conf/server/internal/middleware"
|
||||
"kenaito-conf/server/internal/model"
|
||||
"kenaito-conf/server/internal/service"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ConfigHandler struct {
|
||||
cryptoService *service.CryptoService
|
||||
parseService *service.ConfigParseService
|
||||
permService *service.PermissionService
|
||||
}
|
||||
|
||||
func NewConfigHandler(crypto *service.CryptoService, parse *service.ConfigParseService, perm *service.PermissionService) *ConfigHandler {
|
||||
return &ConfigHandler{cryptoService: crypto, parseService: parse, permService: perm}
|
||||
}
|
||||
|
||||
type ConfigListRequest struct {
|
||||
PageNo int `form:"pageNo" binding:"required"`
|
||||
PageSize int `form:"pageSize" binding:"required"`
|
||||
Env string `form:"env"`
|
||||
AppId *int `form:"appId"`
|
||||
}
|
||||
|
||||
func (h *ConfigHandler) List(c *gin.Context) {
|
||||
var req ConfigListRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.WrapperResponse{Code: 400, Msg: "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
query := dao.Engine.Where("enabled = 1")
|
||||
|
||||
if req.Env != "" {
|
||||
query = query.And("env = ?", req.Env)
|
||||
}
|
||||
if req.AppId != nil {
|
||||
if *req.AppId <= 0 {
|
||||
query = query.And("(app_id is null OR app_id = 0)")
|
||||
} else {
|
||||
var app model.Application
|
||||
if has, _ := dao.Engine.ID(*req.AppId).Get(&app); has {
|
||||
query = query.And("app_code like ?", app.Code+"%")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// permission filter
|
||||
if !userInfo.IsSuperAdmin() {
|
||||
if userInfo.IsGroupAdmin() && userInfo.GroupId != nil {
|
||||
query = query.And("group_id = ?", *userInfo.GroupId)
|
||||
} else if len(userInfo.GrantAppIds) > 0 {
|
||||
query = query.In("app_id", userInfo.GrantAppIds)
|
||||
} else {
|
||||
c.JSON(http.StatusOK, model.PageResult{PageNo: req.PageNo, PageSize: req.PageSize, Total: 0, Data: []interface{}{}})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var configs []model.AppConfig
|
||||
total, _ := query.Count(&model.AppConfig{})
|
||||
query.Limit(req.PageSize, (req.PageNo-1)*req.PageSize).Find(&configs)
|
||||
|
||||
// build app names
|
||||
for i := range configs {
|
||||
configs[i].AppNames = h.buildAppNames(&configs[i])
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.PageResult{PageNo: req.PageNo, PageSize: req.PageSize, Total: total, Data: configs})
|
||||
}
|
||||
|
||||
func (h *ConfigHandler) GetById(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var config model.AppConfig
|
||||
has, _ := dao.Engine.ID(id).Get(&config)
|
||||
if !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 404, Msg: "配置不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
if err := h.permService.RequirePermission(userInfo, config.GroupId, config.AppId, "RO"); err != nil {
|
||||
c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
config.AppNames = h.buildAppNames(&config)
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: config})
|
||||
}
|
||||
|
||||
type AddConfigRequest struct {
|
||||
Id int `json:"id"`
|
||||
GroupId *int `json:"groupId"`
|
||||
AppId *int `json:"appId"`
|
||||
Env string `json:"env" binding:"required"`
|
||||
Version string `json:"version"`
|
||||
Name string `json:"name"`
|
||||
Type int `json:"type" binding:"required"`
|
||||
Contents string `json:"contents"`
|
||||
Global bool `json:"global"`
|
||||
}
|
||||
|
||||
func (h *ConfigHandler) Add(c *gin.Context) {
|
||||
var req AddConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.WrapperResponse{Code: 400, Msg: "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
if req.Global {
|
||||
if req.GroupId == nil {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: "全局配置需要指定所属业务组"})
|
||||
return
|
||||
}
|
||||
if err := h.permService.RequirePermission(userInfo, req.GroupId, nil, "RW"); err != nil {
|
||||
c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !req.Global && req.AppId == nil {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: "非全局绑定应用不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
if req.AppId != nil {
|
||||
if has, _ := dao.Engine.ID(*req.AppId).Get(&app); !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: "应用不存在"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
config := model.AppConfig{
|
||||
GroupId: req.GroupId,
|
||||
AppId: req.AppId,
|
||||
Env: req.Env,
|
||||
Version: req.Version,
|
||||
Name: req.Name,
|
||||
Type: req.Type,
|
||||
Contents: req.Contents,
|
||||
Enabled: true,
|
||||
CreatedAt: func() *time.Time { t := time.Now(); return &t }(),
|
||||
CreatedBy: userInfo.Name,
|
||||
UpdatedAt: func() *time.Time { t := time.Now(); return &t }(),
|
||||
UpdatedBy: userInfo.Name,
|
||||
}
|
||||
|
||||
if req.AppId != nil {
|
||||
config.AppCode = app.Code
|
||||
if !req.Global {
|
||||
config.GroupId = app.GroupId
|
||||
}
|
||||
}
|
||||
|
||||
// encrypt sensitive values
|
||||
h.encryptIfRequired(&config)
|
||||
|
||||
if _, err := dao.Engine.Insert(&config); err != nil {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 500, Msg: "保存失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: true})
|
||||
}
|
||||
|
||||
func (h *ConfigHandler) Update(c *gin.Context) {
|
||||
var req AddConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.WrapperResponse{Code: 400, Msg: "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Id == 0 {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: "id参数缺失"})
|
||||
return
|
||||
}
|
||||
|
||||
var config model.AppConfig
|
||||
has, _ := dao.Engine.ID(req.Id).Get(&config)
|
||||
if !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 404, Msg: "配置不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
if err := h.permService.RequirePermission(userInfo, config.GroupId, config.AppId, "RW"); err != nil {
|
||||
c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// save history
|
||||
h.saveHistory(&config, userInfo.Name)
|
||||
|
||||
originalContents := config.Contents
|
||||
config.Contents = req.Contents
|
||||
config.Version = req.Version
|
||||
config.UpdatedBy = userInfo.Name
|
||||
now := time.Now()
|
||||
config.UpdatedAt = &now
|
||||
|
||||
h.encryptIfRequired(&config)
|
||||
|
||||
dao.Engine.ID(config.Id).Cols("contents", "version", "updated_by", "updated_at").Update(&config)
|
||||
|
||||
// TODO: publish config change event (ZK removed, could use webhook or other mechanism)
|
||||
_ = originalContents
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: true})
|
||||
}
|
||||
|
||||
func (h *ConfigHandler) Delete(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var config model.AppConfig
|
||||
has, _ := dao.Engine.ID(id).Get(&config)
|
||||
if !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 404, Msg: "配置不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
if err := h.permService.RequirePermission(userInfo, config.GroupId, config.AppId, "RW"); err != nil {
|
||||
c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
config.Enabled = false
|
||||
config.UpdatedBy = userInfo.Name
|
||||
now := time.Now()
|
||||
config.UpdatedAt = &now
|
||||
dao.Engine.ID(config.Id).Cols("enabled", "updated_by", "updated_at").Update(&config)
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0})
|
||||
}
|
||||
|
||||
func (h *ConfigHandler) GetHistory(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var histories []model.AppConfigHistory
|
||||
dao.Engine.Where("origin_id = ?", id).OrderBy("id desc").Limit(5).Find(&histories)
|
||||
|
||||
if len(histories) > 0 {
|
||||
var currentConfig model.AppConfig
|
||||
if has, _ := dao.Engine.ID(histories[0].OriginId).Get(¤tConfig); has {
|
||||
for i := range histories {
|
||||
histories[i].ActiveContents = currentConfig.Contents
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: histories})
|
||||
}
|
||||
|
||||
func (h *ConfigHandler) Rollback(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var history model.AppConfigHistory
|
||||
has, _ := dao.Engine.ID(id).Get(&history)
|
||||
if !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 404, Msg: "历史记录不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var config model.AppConfig
|
||||
if has, _ := dao.Engine.ID(history.OriginId).Get(&config); has {
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
if err := h.permService.RequirePermission(userInfo, config.GroupId, config.AppId, "RW"); err != nil {
|
||||
c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
config.Contents = history.Contents
|
||||
config.UpdatedBy = userInfo.Name
|
||||
now := time.Now()
|
||||
config.UpdatedAt = &now
|
||||
dao.Engine.ID(config.Id).Cols("contents", "updated_by", "updated_at").Update(&config)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0})
|
||||
}
|
||||
|
||||
func (h *ConfigHandler) buildAppNames(config *model.AppConfig) string {
|
||||
if config.Global || (config.AppId == nil || *config.AppId == 0) {
|
||||
return "全局配置"
|
||||
}
|
||||
var app model.Application
|
||||
if has, _ := dao.Engine.ID(*config.AppId).Get(&app); has {
|
||||
return app.Name + "(" + app.Code + ")"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (h *ConfigHandler) saveHistory(config *model.AppConfig, operator string) {
|
||||
history := model.AppConfigHistory{
|
||||
OriginId: config.Id,
|
||||
AppId: config.AppId,
|
||||
AppNames: h.buildAppNames(config),
|
||||
Env: config.Env,
|
||||
Version: config.Version,
|
||||
Name: config.Name,
|
||||
Type: config.Type,
|
||||
Contents: config.Contents,
|
||||
CreatedAt: func() *time.Time { t := time.Now(); return &t }(),
|
||||
CreatedBy: operator,
|
||||
}
|
||||
dao.Engine.Insert(&history)
|
||||
}
|
||||
|
||||
func (h *ConfigHandler) encryptIfRequired(config *model.AppConfig) {
|
||||
// simplified: check if contents contain sensitive keys
|
||||
// In production, parse the config and encrypt values with {Cipher} prefix
|
||||
// For now, skip encryption logic
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"kenaito-conf/server/internal/dao"
|
||||
"kenaito-conf/server/internal/model"
|
||||
"kenaito-conf/server/internal/service"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ConfigApiHandler struct {
|
||||
cryptoService *service.CryptoService
|
||||
parseService *service.ConfigParseService
|
||||
}
|
||||
|
||||
func NewConfigApiHandler(crypto *service.CryptoService, parse *service.ConfigParseService) *ConfigApiHandler {
|
||||
return &ConfigApiHandler{cryptoService: crypto, parseService: parse}
|
||||
}
|
||||
|
||||
func (h *ConfigApiHandler) FetchAllConfigs(c *gin.Context) {
|
||||
appName := c.Query("appName")
|
||||
env := c.Query("env")
|
||||
version := c.DefaultQuery("version", "latest")
|
||||
ignoreGlobal := c.DefaultQuery("ignoreGlobal", "false") == "true"
|
||||
|
||||
if appName == "" || env == "" {
|
||||
c.JSON(http.StatusBadRequest, model.WrapperResponse{Code: 400, Msg: "参数缺失"})
|
||||
return
|
||||
}
|
||||
|
||||
// find app by code
|
||||
var app model.Application
|
||||
has, _ := dao.Engine.Where("code = ? AND enabled = 1", appName).Get(&app)
|
||||
if !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 1001, Msg: "配置不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
|
||||
// load global configs
|
||||
if !ignoreGlobal && app.GroupId != nil {
|
||||
var globalConfigs []model.AppConfig
|
||||
dao.Engine.Where("env = ? AND version = ? AND enabled = 1 AND group_id = ? AND (app_id is null OR app_id = 0)",
|
||||
env, version, *app.GroupId).Find(&globalConfigs)
|
||||
for _, cfg := range globalConfigs {
|
||||
kvMap := h.parseService.ParseConfigToKVMap(&cfg)
|
||||
for k, v := range kvMap {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// load parent app configs
|
||||
if app.ParentId != nil {
|
||||
var parentConfigs []model.AppConfig
|
||||
dao.Engine.Where("env = ? AND version = ? AND enabled = 1 AND app_id = ?",
|
||||
env, version, *app.ParentId).Find(&parentConfigs)
|
||||
for _, cfg := range parentConfigs {
|
||||
kvMap := h.parseService.ParseConfigToKVMap(&cfg)
|
||||
for k, v := range kvMap {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// load app configs
|
||||
var appConfigs []model.AppConfig
|
||||
dao.Engine.Where("env = ? AND version = ? AND enabled = 1 AND app_id = ?",
|
||||
env, version, app.Id).Find(&appConfigs)
|
||||
for _, cfg := range appConfigs {
|
||||
kvMap := h.parseService.ParseConfigToKVMap(&cfg)
|
||||
for k, v := range kvMap {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 1001, Msg: "配置不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
// add encryption keys
|
||||
globalCryptKey := h.cryptoService.GetCryptKey(0, env)
|
||||
result["kenaito.config.global-encrypt-secret"] = globalCryptKey
|
||||
appCryptKey := h.cryptoService.GetCryptKey(app.Id, env)
|
||||
result["kenaito.config.encrypt-secret"] = appCryptKey
|
||||
result["latest.fetch.timestamp"] = strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
type FetchChangedRequest struct {
|
||||
AppName string `json:"appName" binding:"required"`
|
||||
Env string `json:"env" binding:"required"`
|
||||
Version string `json:"version"`
|
||||
LastTime int64 `json:"lastTime" binding:"required"`
|
||||
IgnoreGlobal bool `json:"ignoreGlobal"`
|
||||
FetchAll bool `json:"fetchAll"`
|
||||
}
|
||||
|
||||
func (h *ConfigApiHandler) FetchChangedConfigs(c *gin.Context) {
|
||||
var req FetchChangedRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.WrapperResponse{Code: 400, Msg: "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
var app model.Application
|
||||
has, _ := dao.Engine.Where("code = ? AND enabled = 1", req.AppName).Get(&app)
|
||||
if !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 1001, Msg: "应用不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
version := req.Version
|
||||
if version == "" {
|
||||
version = "latest"
|
||||
}
|
||||
|
||||
lastUpdateTime := time.UnixMilli(req.LastTime)
|
||||
|
||||
result := make(map[string]string)
|
||||
|
||||
// load global changed configs
|
||||
if !req.IgnoreGlobal && app.GroupId != nil {
|
||||
var globalConfigs []model.AppConfig
|
||||
dao.Engine.Where("env = ? AND version = ? AND enabled = 1 AND group_id = ? AND (app_id is null OR app_id = 0) AND updated_at > ?",
|
||||
req.Env, version, *app.GroupId, lastUpdateTime).Find(&globalConfigs)
|
||||
for _, cfg := range globalConfigs {
|
||||
h.addChangedConfigs(result, &cfg, req.FetchAll)
|
||||
}
|
||||
}
|
||||
|
||||
// load parent app changed configs
|
||||
if app.ParentId != nil {
|
||||
var parentConfigs []model.AppConfig
|
||||
dao.Engine.Where("env = ? AND version = ? AND enabled = 1 AND app_id = ? AND updated_at > ?",
|
||||
req.Env, version, *app.ParentId, lastUpdateTime).Find(&parentConfigs)
|
||||
for _, cfg := range parentConfigs {
|
||||
h.addChangedConfigs(result, &cfg, req.FetchAll)
|
||||
}
|
||||
}
|
||||
|
||||
// load app changed configs
|
||||
var appConfigs []model.AppConfig
|
||||
dao.Engine.Where("env = ? AND version = ? AND enabled = 1 AND app_id = ? AND updated_at > ?",
|
||||
req.Env, version, app.Id, lastUpdateTime).Find(&appConfigs)
|
||||
for _, cfg := range appConfigs {
|
||||
h.addChangedConfigs(result, &cfg, req.FetchAll)
|
||||
}
|
||||
|
||||
result["latest.fetch.timestamp"] = strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: result})
|
||||
}
|
||||
|
||||
func (h *ConfigApiHandler) addChangedConfigs(result map[string]string, config *model.AppConfig, fetchAll bool) {
|
||||
kvMap := h.parseService.ParseConfigToKVMap(config)
|
||||
for k, v := range kvMap {
|
||||
result[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ConfigApiHandler) Ping(c *gin.Context) {
|
||||
c.String(http.StatusOK, "200")
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"kenaito-conf/server/internal/dao"
|
||||
"kenaito-conf/server/internal/middleware"
|
||||
"kenaito-conf/server/internal/model"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type GroupHandler struct{}
|
||||
|
||||
func NewGroupHandler() *GroupHandler {
|
||||
return &GroupHandler{}
|
||||
}
|
||||
|
||||
func (h *GroupHandler) List(c *gin.Context) {
|
||||
var groups []model.BusinessGroup
|
||||
dao.Engine.Find(&groups)
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: groups})
|
||||
}
|
||||
|
||||
func (h *GroupHandler) GetById(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var group model.BusinessGroup
|
||||
has, _ := dao.Engine.ID(id).Get(&group)
|
||||
if !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 404, Msg: "业务组不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: group})
|
||||
}
|
||||
|
||||
type AddGroupRequest struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
MasterUid *int `json:"masterUid" binding:"required"`
|
||||
Remarks string `json:"remarks"`
|
||||
}
|
||||
|
||||
func (h *GroupHandler) Add(c *gin.Context) {
|
||||
var req AddGroupRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.WrapperResponse{Code: 400, Msg: "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
if !userInfo.IsSuperAdmin() {
|
||||
c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: "超级管理员才能操作"})
|
||||
return
|
||||
}
|
||||
|
||||
var existing model.BusinessGroup
|
||||
if has, _ := dao.Engine.Where("name = ?", req.Name).Get(&existing); has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 1002, Msg: "组名已存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var master model.User
|
||||
if has, _ := dao.Engine.ID(*req.MasterUid).Get(&master); !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: "管理员不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
group := model.BusinessGroup{
|
||||
Name: req.Name,
|
||||
Master: master.Name,
|
||||
MasterUid: req.MasterUid,
|
||||
Remarks: req.Remarks,
|
||||
Enabled: true,
|
||||
CreatedAt: func() *time.Time { t := time.Now(); return &t }(),
|
||||
CreatedBy: userInfo.Name,
|
||||
}
|
||||
|
||||
dao.Engine.Insert(&group)
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0})
|
||||
}
|
||||
|
||||
func (h *GroupHandler) Update(c *gin.Context) {
|
||||
var req AddGroupRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.WrapperResponse{Code: 400, Msg: "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
if !userInfo.IsSuperAdmin() {
|
||||
c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: "超级管理员才能操作"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Id == 0 {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: "id参数缺失"})
|
||||
return
|
||||
}
|
||||
|
||||
var group model.BusinessGroup
|
||||
has, _ := dao.Engine.ID(req.Id).Get(&group)
|
||||
if !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 404, Msg: "业务组不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
group.Name = req.Name
|
||||
group.Remarks = req.Remarks
|
||||
if req.MasterUid != nil && (group.MasterUid == nil || *req.MasterUid != *group.MasterUid) {
|
||||
var master model.User
|
||||
if has, _ := dao.Engine.ID(*req.MasterUid).Get(&master); has {
|
||||
group.Master = master.Name
|
||||
group.MasterUid = req.MasterUid
|
||||
}
|
||||
}
|
||||
group.UpdatedBy = userInfo.Name
|
||||
now := time.Now()
|
||||
group.UpdatedAt = &now
|
||||
|
||||
dao.Engine.ID(group.Id).Cols("name", "remarks", "master", "master_uid", "updated_by", "updated_at").Update(&group)
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0})
|
||||
}
|
||||
|
||||
func (h *GroupHandler) Options(c *gin.Context) {
|
||||
var groups []model.BusinessGroup
|
||||
dao.Engine.Where("enabled = 1").Find(&groups)
|
||||
|
||||
options := make([]model.SelectOption, 0)
|
||||
for _, g := range groups {
|
||||
options = append(options, model.SelectOption{Value: strconv.Itoa(g.Id), Label: g.Name})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: options})
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"kenaito-conf/server/internal/middleware"
|
||||
"kenaito-conf/server/internal/model"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type IndexHandler struct{}
|
||||
|
||||
func NewIndexHandler() *IndexHandler {
|
||||
return &IndexHandler{}
|
||||
}
|
||||
|
||||
func (h *IndexHandler) GetMenus(c *gin.Context) {
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
if userInfo == nil {
|
||||
c.JSON(http.StatusUnauthorized, model.WrapperResponse{Code: 401, Msg: "未登录"})
|
||||
return
|
||||
}
|
||||
|
||||
menus := make([]model.Menu, 0)
|
||||
|
||||
// 全局管理菜单
|
||||
menu1 := model.NewMenu("全局管理", "fa-cubes", false)
|
||||
if userInfo.IsSuperAdmin() {
|
||||
menu1.AddSubMenu("环境管理", "", "/profile/list")
|
||||
}
|
||||
menu1.AddSubMenu("业务组管理", "", "/group/list")
|
||||
if userInfo.IsSuperAdmin() || userInfo.IsGroupAdmin() {
|
||||
menu1.AddSubMenu("用户管理", "", "/user/list")
|
||||
}
|
||||
menu1.AddSubMenu("应用管理", "", "/app/list")
|
||||
menus = append(menus, menu1)
|
||||
|
||||
// 配置管理菜单
|
||||
menu2 := model.NewMenu("配置管理", "fa-cubes", true)
|
||||
menu2.AddSubMenu("配置列表", "", "/config/list")
|
||||
menu2.AddSubMenu("新建配置", "", "/config/add")
|
||||
menus = append(menus, menu2)
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: menus})
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"kenaito-conf/server/internal/dao"
|
||||
"kenaito-conf/server/internal/middleware"
|
||||
"kenaito-conf/server/internal/model"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ProfileHandler struct{}
|
||||
|
||||
func NewProfileHandler() *ProfileHandler {
|
||||
return &ProfileHandler{}
|
||||
}
|
||||
|
||||
func (h *ProfileHandler) List(c *gin.Context) {
|
||||
var profiles []model.Profile
|
||||
dao.Engine.Find(&profiles)
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: profiles})
|
||||
}
|
||||
|
||||
func (h *ProfileHandler) GetById(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var profile model.Profile
|
||||
has, _ := dao.Engine.ID(id).Get(&profile)
|
||||
if !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 404, Msg: "环境不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: profile})
|
||||
}
|
||||
|
||||
type AddProfileRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Alias string `json:"alias"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
}
|
||||
|
||||
func (h *ProfileHandler) Add(c *gin.Context) {
|
||||
var req AddProfileRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.WrapperResponse{Code: 400, Msg: "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
if !userInfo.IsSuperAdmin() {
|
||||
c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: "超级管理员才能操作"})
|
||||
return
|
||||
}
|
||||
|
||||
var existing model.Profile
|
||||
if has, _ := dao.Engine.Where("name = ?", req.Name).Get(&existing); has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 1002, Msg: "环境已存在"})
|
||||
return
|
||||
}
|
||||
|
||||
profile := model.Profile{
|
||||
Name: req.Name,
|
||||
Alias: req.Alias,
|
||||
IsDefault: req.IsDefault,
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
dao.Engine.Insert(&profile)
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: true})
|
||||
}
|
||||
|
||||
type UpdateProfileRequest struct {
|
||||
Id int `json:"id" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Alias string `json:"alias"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
}
|
||||
|
||||
func (h *ProfileHandler) Update(c *gin.Context) {
|
||||
var req UpdateProfileRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.WrapperResponse{Code: 400, Msg: "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
if !userInfo.IsSuperAdmin() {
|
||||
c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: "超级管理员才能操作"})
|
||||
return
|
||||
}
|
||||
|
||||
var profile model.Profile
|
||||
has, _ := dao.Engine.ID(req.Id).Get(&profile)
|
||||
if !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 404, Msg: "环境不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
if profile.Name != req.Name {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: "环境标识不可修改"})
|
||||
return
|
||||
}
|
||||
|
||||
profile.Alias = req.Alias
|
||||
dao.Engine.ID(profile.Id).Update(&profile)
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: true})
|
||||
}
|
||||
|
||||
func (h *ProfileHandler) Delete(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
if !userInfo.IsSuperAdmin() {
|
||||
c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: "超级管理员才能操作"})
|
||||
return
|
||||
}
|
||||
|
||||
var profile model.Profile
|
||||
has, _ := dao.Engine.ID(id).Get(&profile)
|
||||
if !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 404, Msg: "环境不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
if profile.IsDefault {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 1003, Msg: "默认环境不能删除"})
|
||||
return
|
||||
}
|
||||
|
||||
dao.Engine.ID(id).Delete(&model.Profile{})
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0})
|
||||
}
|
||||
|
||||
func (h *ProfileHandler) Switch(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
if !userInfo.IsSuperAdmin() {
|
||||
c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: "超级管理员才能操作"})
|
||||
return
|
||||
}
|
||||
|
||||
var profile model.Profile
|
||||
has, _ := dao.Engine.ID(id).Get(&profile)
|
||||
if !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 404, Msg: "环境不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
profile.Enabled = !profile.Enabled
|
||||
dao.Engine.ID(profile.Id).Cols("enabled").Update(&profile)
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: true})
|
||||
}
|
||||
|
||||
func (h *ProfileHandler) Options(c *gin.Context) {
|
||||
var profiles []model.Profile
|
||||
dao.Engine.Where("enabled = 1").Find(&profiles)
|
||||
|
||||
options := make([]model.SelectOption, 0)
|
||||
for _, p := range profiles {
|
||||
options = append(options, model.SelectOption{Value: p.Name, Label: p.Alias + "(" + p.Name + ")"})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: options})
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"kenaito-conf/server/internal/dao"
|
||||
"kenaito-conf/server/internal/middleware"
|
||||
"kenaito-conf/server/internal/model"
|
||||
"kenaito-conf/server/internal/service"
|
||||
"kenaito-conf/server/internal/util"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
permService *service.PermissionService
|
||||
}
|
||||
|
||||
func NewUserHandler(perm *service.PermissionService) *UserHandler {
|
||||
return &UserHandler{permService: perm}
|
||||
}
|
||||
|
||||
func (h *UserHandler) List(c *gin.Context) {
|
||||
groupIdStr := c.Query("groupId")
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
|
||||
var users []model.User
|
||||
query := dao.Engine.Where("1=1")
|
||||
|
||||
if !userInfo.IsSuperAdmin() && userInfo.GroupId != nil {
|
||||
query = query.And("group_id = ?", *userInfo.GroupId)
|
||||
}
|
||||
if groupIdStr != "" {
|
||||
groupId, _ := strconv.Atoi(groupIdStr)
|
||||
query = query.And("group_id = ?", groupId)
|
||||
}
|
||||
|
||||
query.Find(&users)
|
||||
|
||||
// load group names
|
||||
var groups []model.BusinessGroup
|
||||
dao.Engine.Find(&groups)
|
||||
groupMap := make(map[int]string)
|
||||
for _, g := range groups {
|
||||
groupMap[g.Id] = g.Name
|
||||
}
|
||||
|
||||
for i := range users {
|
||||
if users[i].GroupId != nil {
|
||||
users[i].GroupName = groupMap[*users[i].GroupId]
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: users})
|
||||
}
|
||||
|
||||
type AddUserRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Mobile string `json:"mobile" binding:"required"`
|
||||
Email string `json:"email"`
|
||||
Type string `json:"type" binding:"required"`
|
||||
GroupId *int `json:"groupId"`
|
||||
GroupName string `json:"groupName"`
|
||||
}
|
||||
|
||||
func (h *UserHandler) Add(c *gin.Context) {
|
||||
var req AddUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.WrapperResponse{Code: 400, Msg: "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
|
||||
// validate type - only superAdmin can create superAdmin
|
||||
if req.Type != "user" && req.Type != "groupAdmin" && req.Type != "superAdmin" {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: "无效的用户类型"})
|
||||
return
|
||||
}
|
||||
if req.Type == "superAdmin" && !userInfo.IsSuperAdmin() {
|
||||
c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: "只有超级管理员才能创建超级管理员"})
|
||||
return
|
||||
}
|
||||
|
||||
// check name exists
|
||||
var existing model.User
|
||||
if has, _ := dao.Engine.Where("name = ?", req.Name).Get(&existing); has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 1001, Msg: "用户名已存在"})
|
||||
return
|
||||
}
|
||||
if has, _ := dao.Engine.Where("mobile = ?", req.Mobile).Get(&existing); has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 1001, Msg: "手机号已存在"})
|
||||
return
|
||||
}
|
||||
|
||||
groupId := req.GroupId
|
||||
if groupId == nil && req.GroupName != "" {
|
||||
var group model.BusinessGroup
|
||||
if has, _ := dao.Engine.Where("name = ?", req.GroupName).Get(&group); has {
|
||||
groupId = &group.Id
|
||||
} else {
|
||||
group = model.BusinessGroup{
|
||||
Name: req.GroupName,
|
||||
Enabled: true,
|
||||
CreatedAt: func() *time.Time { t := time.Now(); return &t }(),
|
||||
CreatedBy: userInfo.Name,
|
||||
}
|
||||
dao.Engine.Insert(&group)
|
||||
groupId = &group.Id
|
||||
}
|
||||
}
|
||||
|
||||
password := "admin123"
|
||||
hashedPassword, _ := util.HashPassword(password)
|
||||
|
||||
user := model.User{
|
||||
Name: req.Name,
|
||||
Mobile: req.Mobile,
|
||||
Email: req.Email,
|
||||
Type: req.Type,
|
||||
GroupId: groupId,
|
||||
Password: hashedPassword,
|
||||
Enabled: true,
|
||||
CreatedAt: func() *time.Time { t := time.Now(); return &t }(),
|
||||
}
|
||||
|
||||
dao.Engine.Insert(&user)
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: user})
|
||||
}
|
||||
|
||||
type UpdateUserRequest struct {
|
||||
Email string `json:"email"`
|
||||
Mobile string `json:"mobile"`
|
||||
Password string `json:"password"`
|
||||
OldPassword string `json:"oldPassword"`
|
||||
GroupId *int `json:"groupId"`
|
||||
}
|
||||
|
||||
func (h *UserHandler) Update(c *gin.Context) {
|
||||
var req UpdateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.WrapperResponse{Code: 400, Msg: "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
userId, _ := strconv.Atoi(userInfo.Id)
|
||||
|
||||
var user model.User
|
||||
has, _ := dao.Engine.ID(userId).Get(&user)
|
||||
if !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 404, Msg: "用户不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Email != "" {
|
||||
user.Email = req.Email
|
||||
}
|
||||
if req.Mobile != "" {
|
||||
user.Mobile = req.Mobile
|
||||
}
|
||||
if req.Password != "" {
|
||||
if err := util.VerifyPassword(user.Password, req.OldPassword); err != nil {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 1001, Msg: "原密码不正确"})
|
||||
return
|
||||
}
|
||||
hashedPassword, _ := util.HashPassword(req.Password)
|
||||
user.Password = hashedPassword
|
||||
}
|
||||
if req.GroupId != nil && *req.GroupId > 0 {
|
||||
user.GroupId = req.GroupId
|
||||
}
|
||||
user.UpdatedAt = func() *time.Time { t := time.Now(); return &t }()
|
||||
|
||||
dao.Engine.ID(user.Id).Cols("email", "mobile", "password", "group_id", "updated_at").Update(&user)
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: true})
|
||||
}
|
||||
|
||||
type AdminUpdateUserRequest struct {
|
||||
Id int `json:"id" binding:"required"`
|
||||
Email string `json:"email"`
|
||||
Mobile string `json:"mobile"`
|
||||
Type string `json:"type"`
|
||||
GroupId *int `json:"groupId"`
|
||||
GroupName string `json:"groupName"`
|
||||
}
|
||||
|
||||
func (h *UserHandler) AdminUpdate(c *gin.Context) {
|
||||
var req AdminUpdateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.WrapperResponse{Code: 400, Msg: "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
if !userInfo.IsSuperAdmin() {
|
||||
c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: "只有超级管理员才能编辑用户"})
|
||||
return
|
||||
}
|
||||
|
||||
var user model.User
|
||||
has, _ := dao.Engine.ID(req.Id).Get(&user)
|
||||
if !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 404, Msg: "用户不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
// superAdmin 不能修改类型和所属组
|
||||
if user.Type == "superAdmin" {
|
||||
if req.Type != "" && req.Type != "superAdmin" {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: "不能修改超级管理员的类型"})
|
||||
return
|
||||
}
|
||||
if req.GroupId != nil || req.GroupName != "" {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: "不能修改超级管理员的所属组"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.Mobile != "" {
|
||||
user.Mobile = req.Mobile
|
||||
}
|
||||
if req.Email != "" {
|
||||
user.Email = req.Email
|
||||
}
|
||||
if req.Type != "" && user.Type != "superAdmin" {
|
||||
user.Type = req.Type
|
||||
}
|
||||
|
||||
// 处理所属组
|
||||
if user.Type != "superAdmin" {
|
||||
if req.GroupId != nil && *req.GroupId > 0 {
|
||||
user.GroupId = req.GroupId
|
||||
} else if req.GroupName != "" {
|
||||
var group model.BusinessGroup
|
||||
has, _ := dao.Engine.Where("name = ?", req.GroupName).Get(&group)
|
||||
if !has {
|
||||
newGroup := model.BusinessGroup{
|
||||
Name: req.GroupName,
|
||||
Enabled: true,
|
||||
CreatedAt: func() *time.Time { t := time.Now(); return &t }(),
|
||||
CreatedBy: userInfo.Name,
|
||||
}
|
||||
dao.Engine.Insert(&newGroup)
|
||||
user.GroupId = &newGroup.Id
|
||||
} else {
|
||||
user.GroupId = &group.Id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user.UpdatedAt = func() *time.Time { t := time.Now(); return &t }()
|
||||
dao.Engine.ID(user.Id).Cols("mobile", "email", "type", "group_id", "updated_at").Update(&user)
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: true})
|
||||
}
|
||||
|
||||
func (h *UserHandler) Delete(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
|
||||
var user model.User
|
||||
has, _ := dao.Engine.ID(id).Get(&user)
|
||||
if !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 404, Msg: "用户不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.permService.HasPermissionForUser(userInfo, &user); err != nil {
|
||||
c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
dao.Engine.ID(id).Delete(&model.User{})
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0})
|
||||
}
|
||||
|
||||
func (h *UserHandler) ResetPassword(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var user model.User
|
||||
has, _ := dao.Engine.ID(id).Get(&user)
|
||||
if !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 404, Msg: "用户不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
newPassword := randomString(8)
|
||||
hashedPassword, _ := util.HashPassword(newPassword)
|
||||
user.Password = hashedPassword
|
||||
now := time.Now()
|
||||
user.UpdatedAt = &now
|
||||
dao.Engine.ID(user.Id).Cols("password", "updated_at").Update(&user)
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: newPassword})
|
||||
}
|
||||
|
||||
func (h *UserHandler) Switch(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var user model.User
|
||||
has, _ := dao.Engine.ID(id).Get(&user)
|
||||
if !has {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 404, Msg: "用户不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
// 用户名为 admin 的用户不能被禁用
|
||||
if user.Name == "admin" && user.Enabled {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: "admin 用户不能被禁用"})
|
||||
return
|
||||
}
|
||||
|
||||
user.Enabled = !user.Enabled
|
||||
dao.Engine.ID(user.Id).Cols("enabled").Update(&user)
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: true})
|
||||
}
|
||||
|
||||
func (h *UserHandler) Options(c *gin.Context) {
|
||||
groupIdStr := c.Query("groupId")
|
||||
var users []model.User
|
||||
query := dao.Engine.Where("enabled = 1")
|
||||
if groupIdStr != "" {
|
||||
groupId, _ := strconv.Atoi(groupIdStr)
|
||||
query = query.And("group_id = ?", groupId)
|
||||
}
|
||||
query.Find(&users)
|
||||
|
||||
options := make([]model.SelectOption, 0)
|
||||
for _, u := range users {
|
||||
options = append(options, model.SelectOption{Value: strconv.Itoa(u.Id), Label: u.Name})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: options})
|
||||
}
|
||||
|
||||
func randomString(n int) string {
|
||||
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func CORS() gin.HandlerFunc {
|
||||
return cors.New(cors.Config{
|
||||
AllowOrigins: []string{"*"},
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
|
||||
ExposeHeaders: []string{"Content-Length"},
|
||||
AllowCredentials: false,
|
||||
MaxAge: 12 * time.Hour,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"kenaito-conf/server/internal/model"
|
||||
"kenaito-conf/server/internal/service"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func JWTAuth(authService *service.AuthService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "未登录"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
userInfo, err := authService.ValidateToken(tokenString)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "登录已过期"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("userInfo", userInfo)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func GetUserInfo(c *gin.Context) *model.LoginUserInfo {
|
||||
val, exists := c.Get("userInfo")
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
return val.(*model.LoginUserInfo)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type AppConfig struct {
|
||||
Id int `xorm:"int autoincr pk" json:"id"`
|
||||
GroupId *int `xorm:"int" json:"groupId"`
|
||||
AppId *int `xorm:"int" json:"appId"`
|
||||
AppCode string `xorm:"varchar(32)" json:"appCode"`
|
||||
Env string `xorm:"varchar(16)" json:"env"`
|
||||
Version string `xorm:"varchar(32)" json:"version"`
|
||||
Name string `xorm:"varchar(32)" json:"name"`
|
||||
Type int `xorm:"smallint(1)" json:"type"` // 1:file, 2:kv, 3:json
|
||||
Contents string `xorm:"text" json:"contents"`
|
||||
Enabled bool `xorm:"tinyint(1) default 1" json:"enabled"`
|
||||
CreatedAt *time.Time `xorm:"datetime" json:"createdAt"`
|
||||
CreatedBy string `xorm:"varchar(32)" json:"createdBy"`
|
||||
UpdatedAt *time.Time `xorm:"datetime" json:"updatedAt"`
|
||||
UpdatedBy string `xorm:"varchar(32)" json:"updatedBy"`
|
||||
Global bool `xorm:"-" json:"global"`
|
||||
AppNames string `xorm:"-" json:"appNames,omitempty"`
|
||||
}
|
||||
|
||||
type AppConfigHistory struct {
|
||||
Id int `xorm:"int autoincr pk" json:"id"`
|
||||
OriginId int `xorm:"int" json:"originId"`
|
||||
AppId *int `xorm:"int" json:"appId"`
|
||||
AppName string `xorm:"varchar(32)" json:"appName"`
|
||||
AppNames string `xorm:"varchar(64)" json:"appNames"`
|
||||
Env string `xorm:"varchar(16)" json:"env"`
|
||||
Version string `xorm:"varchar(32)" json:"version"`
|
||||
Name string `xorm:"varchar(32)" json:"name"`
|
||||
Type int `xorm:"smallint(1)" json:"type"`
|
||||
Contents string `xorm:"text" json:"contents"`
|
||||
ActiveContents string `xorm:"-" json:"activeContents,omitempty"`
|
||||
CreatedAt *time.Time `xorm:"datetime" json:"createdAt"`
|
||||
CreatedBy string `xorm:"varchar(32)" json:"createdBy"`
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type Application struct {
|
||||
Id int `xorm:"int autoincr pk" json:"id"`
|
||||
GroupId *int `xorm:"int" json:"groupId"`
|
||||
ParentId *int `xorm:"int" json:"parentId"`
|
||||
Name string `xorm:"varchar(32)" json:"name"`
|
||||
Code string `xorm:"varchar(32)" json:"code"`
|
||||
ServiceId string `xorm:"varchar(64)" json:"serviceId"`
|
||||
IsModule bool `xorm:"tinyint(1) default 1" json:"isModule"`
|
||||
OwnerId *int `xorm:"int" json:"ownerId"`
|
||||
OwnerName string `xorm:"varchar(16)" json:"ownerName"`
|
||||
Remarks string `xorm:"text" json:"remarks"`
|
||||
Enabled bool `xorm:"tinyint(1) default 1" json:"enabled"`
|
||||
CreatedAt *time.Time `xorm:"datetime" json:"createdAt"`
|
||||
CreatedBy string `xorm:"varchar(32)" json:"createdBy"`
|
||||
UpdatedAt *time.Time `xorm:"datetime" json:"updatedAt"`
|
||||
UpdatedBy string `xorm:"varchar(32)" json:"updatedBy"`
|
||||
ParentName string `xorm:"-" json:"parentName,omitempty"`
|
||||
FullName string `xorm:"-" json:"fullName,omitempty"`
|
||||
GroupName string `xorm:"-" json:"groupName,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type BusinessGroup struct {
|
||||
Id int `xorm:"int autoincr pk" json:"id"`
|
||||
Name string `xorm:"varchar(32) notnull" json:"name"`
|
||||
Master string `xorm:"varchar(16)" json:"master"`
|
||||
MasterUid *int `xorm:"int" json:"masterUid"`
|
||||
Remarks string `xorm:"text" json:"remarks"`
|
||||
Enabled bool `xorm:"tinyint(1) default 1" json:"enabled"`
|
||||
CreatedAt *time.Time `xorm:"datetime" json:"createdAt"`
|
||||
CreatedBy string `xorm:"varchar(32)" json:"createdBy"`
|
||||
UpdatedAt *time.Time `xorm:"datetime" json:"updatedAt"`
|
||||
UpdatedBy string `xorm:"varchar(32)" json:"updatedBy"`
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package model
|
||||
|
||||
type Profile struct {
|
||||
Id int `xorm:"int autoincr pk" json:"id"`
|
||||
Name string `xorm:"varchar(32)" json:"name"`
|
||||
Alias string `xorm:"varchar(32)" json:"alias"`
|
||||
IsDefault bool `xorm:"tinyint(1) default 0" json:"isDefault"`
|
||||
Enabled bool `xorm:"tinyint(1) default 1" json:"enabled"`
|
||||
}
|
||||
|
||||
type AppExtrAttr struct {
|
||||
AppId int `xorm:"int pk" json:"appId"`
|
||||
Env string `xorm:"varchar(16) pk" json:"env"`
|
||||
AttrName string `xorm:"varchar(32) pk" json:"attrName"`
|
||||
AttrValue string `xorm:"varchar(100)" json:"attrValue"`
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package model
|
||||
|
||||
type WrapperResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg,omitempty"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type PageResult struct {
|
||||
PageNo int `json:"pageNo"`
|
||||
PageSize int `json:"pageSize"`
|
||||
Total int64 `json:"total"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type SelectOption struct {
|
||||
Value string `json:"value"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
type Menu struct {
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon"`
|
||||
Active bool `json:"active"`
|
||||
Children []SubMenu `json:"children"`
|
||||
}
|
||||
|
||||
type SubMenu struct {
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
func NewMenu(title, icon string, active bool) Menu {
|
||||
return Menu{Title: title, Icon: icon, Active: active, Children: make([]SubMenu, 0)}
|
||||
}
|
||||
|
||||
func (m *Menu) AddSubMenu(title, icon, url string) {
|
||||
m.Children = append(m.Children, SubMenu{Title: title, Icon: icon, Url: url})
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type User struct {
|
||||
Id int `xorm:"int autoincr pk" json:"id"`
|
||||
GroupId *int `xorm:"int" json:"groupId"`
|
||||
Name string `xorm:"varchar(16)" json:"name"`
|
||||
Password string `xorm:"varchar(64)" json:"-"`
|
||||
Mobile string `xorm:"char(11)" json:"mobile"`
|
||||
Email string `xorm:"varchar(32)" json:"email"`
|
||||
Type string `xorm:"varchar(16) default 'user'" json:"type"` // user, groupAdmin, superAdmin
|
||||
Enabled bool `xorm:"tinyint(1) default 1" json:"enabled"`
|
||||
CreatedAt *time.Time `xorm:"datetime" json:"createdAt"`
|
||||
UpdatedAt *time.Time `xorm:"datetime" json:"updatedAt"`
|
||||
GroupName string `xorm:"-" json:"groupName,omitempty"`
|
||||
GroupMaster bool `xorm:"-" json:"groupMaster,omitempty"`
|
||||
}
|
||||
|
||||
type UserPermission struct {
|
||||
Id int `xorm:"int autoincr pk" json:"id"`
|
||||
UserId int `xorm:"int" json:"userId"`
|
||||
Env string `xorm:"varchar(16)" json:"env"`
|
||||
AppId int `xorm:"int" json:"appId"`
|
||||
Operate string `xorm:"varchar(8) default 'RW'" json:"operate"` // RO, RW
|
||||
}
|
||||
|
||||
type LoginUserInfo struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
GroupId *int `json:"groupId"`
|
||||
GrantAppIds []int `json:"grantAppIds"`
|
||||
GrantPermissons []string `json:"grantPermissons"`
|
||||
GroupMaster bool `json:"groupMaster"`
|
||||
}
|
||||
|
||||
func (u *LoginUserInfo) IsSuperAdmin() bool {
|
||||
return u.Type == "superAdmin"
|
||||
}
|
||||
|
||||
func (u *LoginUserInfo) IsGroupAdmin() bool {
|
||||
return u.Type == "groupAdmin"
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"kenaito-conf/server/internal/dao"
|
||||
"kenaito-conf/server/internal/model"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type AuthService struct {
|
||||
jwtSecret string
|
||||
expireHrs int
|
||||
}
|
||||
|
||||
func NewAuthService(secret string, expireHours int) *AuthService {
|
||||
return &AuthService{jwtSecret: secret, expireHrs: expireHours}
|
||||
}
|
||||
|
||||
func (s *AuthService) Login(userName, password string) (string, *model.LoginUserInfo, error) {
|
||||
var user model.User
|
||||
has, err := dao.Engine.Where("name = ? AND enabled = 1", userName).Get(&user)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if !has {
|
||||
return "", nil, errors.New("用户不存在或已禁用")
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
||||
return "", nil, errors.New("密码错误")
|
||||
}
|
||||
|
||||
userInfo := &model.LoginUserInfo{
|
||||
Id: strconv.Itoa(user.Id),
|
||||
Name: user.Name,
|
||||
Type: user.Type,
|
||||
GroupId: user.GroupId,
|
||||
GrantAppIds: make([]int, 0),
|
||||
GrantPermissons: make([]string, 0),
|
||||
}
|
||||
|
||||
// load permissions
|
||||
s.loadPermissions(userInfo, user.Id)
|
||||
|
||||
// generate JWT
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"userId": user.Id,
|
||||
"name": user.Name,
|
||||
"type": user.Type,
|
||||
"groupId": user.GroupId,
|
||||
"exp": time.Now().Add(time.Duration(s.expireHrs) * time.Hour).Unix(),
|
||||
})
|
||||
tokenString, err := token.SignedString([]byte(s.jwtSecret))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return tokenString, userInfo, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) ValidateToken(tokenString string) (*model.LoginUserInfo, error) {
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(s.jwtSecret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok || !token.Valid {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
userId := int(claims["userId"].(float64))
|
||||
var user model.User
|
||||
has, err := dao.Engine.ID(userId).Get(&user)
|
||||
if err != nil || !has {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
userInfo := &model.LoginUserInfo{
|
||||
Id: strconv.Itoa(user.Id),
|
||||
Name: user.Name,
|
||||
Type: user.Type,
|
||||
GroupId: user.GroupId,
|
||||
GrantAppIds: make([]int, 0),
|
||||
GrantPermissons: make([]string, 0),
|
||||
}
|
||||
s.loadPermissions(userInfo, user.Id)
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) loadPermissions(userInfo *model.LoginUserInfo, userId int) {
|
||||
if userInfo.IsSuperAdmin() {
|
||||
return
|
||||
}
|
||||
|
||||
// load owned apps
|
||||
var apps []model.Application
|
||||
if userInfo.IsGroupAdmin() {
|
||||
dao.Engine.Where("group_id = ?", userInfo.GroupId).Find(&apps)
|
||||
} else {
|
||||
dao.Engine.Where("owner_id = ?", userId).Find(&apps)
|
||||
}
|
||||
for _, app := range apps {
|
||||
userInfo.GrantAppIds = append(userInfo.GrantAppIds, app.Id)
|
||||
userInfo.GrantPermissons = append(userInfo.GrantPermissons, buildPermissionCode(app.Id, "RW"))
|
||||
}
|
||||
|
||||
// load granted permissions
|
||||
var perms []model.UserPermission
|
||||
dao.Engine.Where("user_id = ?", userId).Find(&perms)
|
||||
for _, p := range perms {
|
||||
permCode := buildPermissionCode(p.AppId, p.Operate)
|
||||
// avoid duplicate
|
||||
found := false
|
||||
for _, existing := range userInfo.GrantPermissons {
|
||||
if existing == permCode {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
userInfo.GrantPermissons = append(userInfo.GrantPermissons, permCode)
|
||||
}
|
||||
appIdFound := false
|
||||
for _, id := range userInfo.GrantAppIds {
|
||||
if id == p.AppId {
|
||||
appIdFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !appIdFound {
|
||||
userInfo.GrantAppIds = append(userInfo.GrantAppIds, p.AppId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildPermissionCode(appId int, operate string) string {
|
||||
return strconv.Itoa(appId) + ":" + operate
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"kenaito-conf/server/internal/model"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type ConfigParseService struct{}
|
||||
|
||||
func NewConfigParseService() *ConfigParseService {
|
||||
return &ConfigParseService{}
|
||||
}
|
||||
|
||||
// ParseConfigToKVMap parses config content based on type
|
||||
func (s *ConfigParseService) ParseConfigToKVMap(config *model.AppConfig) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
switch config.Type {
|
||||
case 1: // file
|
||||
if strings.HasSuffix(strings.ToLower(config.Name), ".xml") {
|
||||
s.parseXML(result, config.Contents)
|
||||
} else if strings.HasSuffix(strings.ToLower(config.Name), ".yml") ||
|
||||
strings.HasSuffix(strings.ToLower(config.Name), ".yaml") {
|
||||
s.parseYAML(result, config.Contents)
|
||||
} else {
|
||||
s.parseProperties(result, config.Contents)
|
||||
}
|
||||
case 2: // key-value
|
||||
result[config.Name] = config.Contents
|
||||
case 3: // JSON
|
||||
json.Unmarshal([]byte(config.Contents), &result)
|
||||
}
|
||||
|
||||
// resolve placeholders
|
||||
s.resolvePlaceholders(result)
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *ConfigParseService) parseProperties(result map[string]interface{}, content string) {
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
idx := strings.Index(line, "=")
|
||||
if idx < 0 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(line[:idx])
|
||||
value := strings.TrimSpace(line[idx+1:])
|
||||
if key != "" {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ConfigParseService) parseXML(result map[string]interface{}, content string) {
|
||||
// simple XML parsing for <entry key="...">value</entry> format
|
||||
// This is a simplified version - for production use a proper XML parser
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if !strings.Contains(line, "<entry") {
|
||||
continue
|
||||
}
|
||||
// extract key
|
||||
keyStart := strings.Index(line, `key="`)
|
||||
if keyStart < 0 {
|
||||
continue
|
||||
}
|
||||
keyStart += 5
|
||||
keyEnd := strings.Index(line[keyStart:], `"`)
|
||||
if keyEnd < 0 {
|
||||
continue
|
||||
}
|
||||
key := line[keyStart : keyStart+keyEnd]
|
||||
|
||||
// extract value
|
||||
valStart := strings.Index(line, ">")
|
||||
if valStart < 0 {
|
||||
continue
|
||||
}
|
||||
valEnd := strings.Index(line, "</entry>")
|
||||
if valEnd < 0 {
|
||||
continue
|
||||
}
|
||||
value := strings.TrimSpace(line[valStart+1 : valEnd])
|
||||
if key != "" && value != "" {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ConfigParseService) parseYAML(result map[string]interface{}, content string) {
|
||||
var data map[string]interface{}
|
||||
if err := yaml.Unmarshal([]byte(content), &data); err != nil {
|
||||
return
|
||||
}
|
||||
s.flattenYAML("", result, data)
|
||||
}
|
||||
|
||||
func (s *ConfigParseService) flattenYAML(prefix string, result map[string]interface{}, data map[string]interface{}) {
|
||||
for k, v := range data {
|
||||
key := k
|
||||
if prefix != "" {
|
||||
key = prefix + "." + k
|
||||
}
|
||||
if subMap, ok := v.(map[string]interface{}); ok {
|
||||
s.flattenYAML(key, result, subMap)
|
||||
} else {
|
||||
result[key] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ConfigParseService) resolvePlaceholders(result map[string]interface{}) {
|
||||
for key, val := range result {
|
||||
strVal, ok := val.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(strVal, "${") {
|
||||
result[key] = s.replacePlaceholder(result, strVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ConfigParseService) replacePlaceholder(data map[string]interface{}, value string) string {
|
||||
result := value
|
||||
maxIterations := 100
|
||||
for i := 0; i < maxIterations; i++ {
|
||||
start := strings.Index(result, "${")
|
||||
if start < 0 {
|
||||
break
|
||||
}
|
||||
end := strings.Index(result[start:], "}")
|
||||
if end < 0 {
|
||||
break
|
||||
}
|
||||
end += start
|
||||
|
||||
placeholder := result[start+2 : end]
|
||||
defaultVal := ""
|
||||
refKey := placeholder
|
||||
|
||||
// check for default value
|
||||
if idx := strings.Index(placeholder, ":"); idx >= 0 {
|
||||
refKey = placeholder[:idx]
|
||||
defaultVal = placeholder[idx+1:]
|
||||
}
|
||||
|
||||
var replacement string
|
||||
if v, ok := data[refKey]; ok {
|
||||
replacement = fmt.Sprintf("%v", v)
|
||||
} else if defaultVal != "" {
|
||||
replacement = defaultVal
|
||||
} else {
|
||||
replacement = "${" + placeholder + "}"
|
||||
}
|
||||
|
||||
result = result[:start] + replacement + result[end+1:]
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"kenaito-conf/server/internal/config"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const CryptPrefix = "{Cipher}"
|
||||
|
||||
type CryptoService struct{}
|
||||
|
||||
func NewCryptoService() *CryptoService {
|
||||
return &CryptoService{}
|
||||
}
|
||||
|
||||
func (s *CryptoService) GetCryptKey(appId int, env string) string {
|
||||
base := config.Cfg.Crypto.SecretKey + fmt.Sprintf("%x", appId) + env
|
||||
h := md5.Sum([]byte(base))
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
func (s *CryptoService) Encrypt(appId int, env string, data string) string {
|
||||
key := s.GetCryptKey(appId, env)
|
||||
data = strings.ReplaceAll(data, CryptPrefix, "")
|
||||
encrypted := encryptWithAES(key, data)
|
||||
return CryptPrefix + encrypted
|
||||
}
|
||||
|
||||
func (s *CryptoService) IsEncrypted(appId int, env string, data string) bool {
|
||||
if !strings.HasPrefix(data, CryptPrefix) {
|
||||
return false
|
||||
}
|
||||
key := s.GetCryptKey(appId, env)
|
||||
_, err := decryptWithAES(key, strings.TrimPrefix(data, CryptPrefix))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (s *CryptoService) Decrypt(secret string, data string) (string, error) {
|
||||
return decryptWithAES(secret, data)
|
||||
}
|
||||
|
||||
func encryptWithAES(key string, data string) string {
|
||||
secretKey := md5.Sum([]byte(key))
|
||||
aesKey := hex.EncodeToString(secretKey[:])[16:]
|
||||
|
||||
block, err := aes.NewCipher([]byte(aesKey))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// ECB mode (compatible with Java version)
|
||||
plainText := pkcs7Pad([]byte(data), aes.BlockSize)
|
||||
encrypted := make([]byte, len(plainText))
|
||||
mode := cipher.NewCBCEncrypter(block, make([]byte, aes.BlockSize))
|
||||
mode.CryptBlocks(encrypted, plainText)
|
||||
|
||||
return base64.StdEncoding.EncodeToString(encrypted)
|
||||
}
|
||||
|
||||
func decryptWithAES(key string, data string) (string, error) {
|
||||
secretKey := md5.Sum([]byte(key))
|
||||
aesKey := hex.EncodeToString(secretKey[:])[16:]
|
||||
|
||||
cipherText, err := base64.StdEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher([]byte(aesKey))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(cipherText)%aes.BlockSize != 0 {
|
||||
return "", fmt.Errorf("ciphertext is not a multiple of block size")
|
||||
}
|
||||
|
||||
// ECB mode (compatible with Java version)
|
||||
plainText := make([]byte, len(cipherText))
|
||||
mode := cipher.NewCBCDecrypter(block, make([]byte, aes.BlockSize))
|
||||
mode.CryptBlocks(plainText, cipherText)
|
||||
|
||||
plainText, err = pkcs7Unpad(plainText, aes.BlockSize)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(plainText), nil
|
||||
}
|
||||
|
||||
func pkcs7Pad(data []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(data)%blockSize
|
||||
padText := make([]byte, padding)
|
||||
for i := range padText {
|
||||
padText[i] = byte(padding)
|
||||
}
|
||||
return append(data, padText...)
|
||||
}
|
||||
|
||||
func pkcs7Unpad(data []byte, blockSize int) ([]byte, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("empty data")
|
||||
}
|
||||
padding := int(data[len(data)-1])
|
||||
if padding > blockSize || padding == 0 {
|
||||
return nil, fmt.Errorf("invalid padding")
|
||||
}
|
||||
return data[:len(data)-padding], nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"kenaito-conf/server/internal/model"
|
||||
)
|
||||
|
||||
type PermissionService struct{}
|
||||
|
||||
func NewPermissionService() *PermissionService {
|
||||
return &PermissionService{}
|
||||
}
|
||||
|
||||
func (s *PermissionService) RequireSuperAdmin(userInfo *model.LoginUserInfo) error {
|
||||
if !userInfo.IsSuperAdmin() {
|
||||
return errors.New("超级管理员才有权限操作")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PermissionService) RequirePermission(userInfo *model.LoginUserInfo, groupId *int, appId *int, operate string) error {
|
||||
if userInfo.IsSuperAdmin() {
|
||||
return nil
|
||||
}
|
||||
if groupId != nil && userInfo.GroupId != nil && *groupId == *userInfo.GroupId {
|
||||
return nil
|
||||
}
|
||||
if appId == nil || *appId == 0 {
|
||||
return errors.New("你没有全局配置权限")
|
||||
}
|
||||
permCode := buildPermissionCode(*appId, operate)
|
||||
for _, p := range userInfo.GrantPermissons {
|
||||
if p == permCode {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("你没有该应用权限")
|
||||
}
|
||||
|
||||
func (s *PermissionService) HasPermissionForUser(userInfo *model.LoginUserInfo, targetUser *model.User) error {
|
||||
if userInfo.IsSuperAdmin() {
|
||||
return nil
|
||||
}
|
||||
if !userInfo.IsGroupAdmin() {
|
||||
return errors.New("你不是业务组管理员")
|
||||
}
|
||||
if userInfo.GroupId == nil || targetUser.GroupId == nil || *userInfo.GroupId != *targetUser.GroupId {
|
||||
return errors.New("不能操作其他组成员")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package util
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
fromPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(fromPassword), err
|
||||
}
|
||||
func VerifyPassword(hashedPassword, password string) error {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||
}
|
||||
Reference in New Issue
Block a user