mirror of
https://github.com/veops/oneterm.git
synced 2025-10-06 07:47:12 +08:00
refactor
This commit is contained in:
439
backend/api/controller/controller.go
Normal file
439
backend/api/controller/controller.go
Normal file
@@ -0,0 +1,439 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/veops/oneterm/acl"
|
||||
mysql "github.com/veops/oneterm/db"
|
||||
"github.com/veops/oneterm/model"
|
||||
"github.com/veops/oneterm/remote"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultHttpResponse = &HttpResponse{
|
||||
Code: 0,
|
||||
Message: "ok",
|
||||
Data: nil,
|
||||
}
|
||||
)
|
||||
|
||||
type preHook[T any] func(*gin.Context, T)
|
||||
type postHook[T any] func(*gin.Context, []T)
|
||||
type deleteCheck func(*gin.Context, int)
|
||||
|
||||
type Controller struct{}
|
||||
|
||||
func NewController() *Controller {
|
||||
return &Controller{}
|
||||
}
|
||||
|
||||
type HttpResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
type ListData struct {
|
||||
Count int64 `json:"count"`
|
||||
List []any `json:"list"`
|
||||
}
|
||||
|
||||
func NewHttpResponseWithData(data any) *HttpResponse {
|
||||
return &HttpResponse{
|
||||
Code: 0,
|
||||
Message: "ok",
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func doCreate[T model.Model](ctx *gin.Context, needAcl bool, md T, resourceType string, preHooks ...preHook[T]) (err error) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
|
||||
if err = ctx.BindJSON(md); err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
for _, hook := range preHooks {
|
||||
if hook == nil {
|
||||
continue
|
||||
}
|
||||
hook(ctx, md)
|
||||
if ctx.IsAborted() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resourceId := 0
|
||||
if needAcl {
|
||||
if !acl.IsAdmin(currentUser) {
|
||||
ctx.AbortWithError(http.StatusForbidden, &ApiError{Code: ErrNoPerm, Data: map[string]any{"perm": acl.WRITE}})
|
||||
return
|
||||
}
|
||||
resourceId, err = acl.CreateGrantAcl(ctx, currentUser, resourceType, md.GetName())
|
||||
if err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
}
|
||||
md.SetResourceId(resourceId)
|
||||
}
|
||||
|
||||
md.SetCreatorId(currentUser.Uid)
|
||||
md.SetUpdaterId(currentUser.Uid)
|
||||
|
||||
if err = mysql.DB.Transaction(func(tx *gorm.DB) (err error) {
|
||||
if err = tx.Model(md).Create(md).Error; err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch t := any(md).(type) {
|
||||
case *model.Asset:
|
||||
if err = HandleAuthorization(currentUser, tx, model.ACTION_CREATE, nil, t); err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = tx.Create(&model.History{
|
||||
RemoteIp: ctx.RemoteIP(),
|
||||
Type: md.TableName(),
|
||||
TargetId: md.GetId(),
|
||||
ActionType: model.ACTION_CREATE,
|
||||
Old: nil,
|
||||
New: toMap(md),
|
||||
CreatorId: currentUser.Uid,
|
||||
CreatedAt: time.Now(),
|
||||
}).Error; err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}); err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, defaultHttpResponse)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func doDelete[T model.Model](ctx *gin.Context, needAcl bool, md T, dcs ...deleteCheck) (err error) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
id, err := cast.ToIntE(ctx.Param("id"))
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
if err = mysql.DB.Model(md).Where("id = ?", id).First(md).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
ctx.JSON(http.StatusOK, defaultHttpResponse)
|
||||
return
|
||||
}
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
if needAcl {
|
||||
if !acl.IsAdmin(currentUser) && acl.HasPerm(md.GetResourceId(), currentUser.Acl.Rid, acl.DELETE) {
|
||||
ctx.AbortWithError(http.StatusForbidden, &ApiError{Code: ErrNoPerm, Data: map[string]any{"perm": acl.DELETE}})
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, dc := range dcs {
|
||||
if dc == nil {
|
||||
continue
|
||||
}
|
||||
dc(ctx, id)
|
||||
if ctx.IsAborted() {
|
||||
return
|
||||
}
|
||||
}
|
||||
if needAcl {
|
||||
if err = acl.DeleteResource(ctx, currentUser.GetUid(), md.GetResourceId()); err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = mysql.DB.Transaction(func(tx *gorm.DB) (err error) {
|
||||
switch t := any(md).(type) {
|
||||
case *model.Asset:
|
||||
if err = HandleAuthorization(currentUser, tx, model.ACTION_DELETE, t, nil); err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = tx.Delete(md, id).Error; err != nil {
|
||||
return
|
||||
}
|
||||
err = tx.Create(&model.History{
|
||||
RemoteIp: ctx.ClientIP(),
|
||||
Type: md.TableName(),
|
||||
TargetId: md.GetId(),
|
||||
ActionType: model.ACTION_DELETE,
|
||||
Old: toMap(md),
|
||||
New: nil,
|
||||
CreatorId: currentUser.Uid,
|
||||
CreatedAt: time.Now(),
|
||||
}).Error
|
||||
return
|
||||
}); err != nil {
|
||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrDuplicateName, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, defaultHttpResponse)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func doUpdate[T model.Model](ctx *gin.Context, needAcl bool, md T, preHooks ...preHook[T]) (err error) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
|
||||
id, err := cast.ToIntE(ctx.Param("id"))
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
if err = ctx.BindJSON(md); err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
md.SetUpdaterId(currentUser.Uid)
|
||||
|
||||
for _, hook := range preHooks {
|
||||
if hook == nil {
|
||||
continue
|
||||
}
|
||||
hook(ctx, md)
|
||||
if ctx.IsAborted() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
old := getEmpty(md)
|
||||
if err = mysql.DB.Model(md).Where("id = ?", id).First(old).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
ctx.JSON(http.StatusOK, defaultHttpResponse)
|
||||
return
|
||||
}
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
if needAcl {
|
||||
if !acl.IsAdmin(currentUser) && acl.HasPerm(md.GetResourceId(), currentUser.Acl.Rid, acl.WRITE) {
|
||||
ctx.AbortWithError(http.StatusForbidden, &ApiError{Code: ErrNoPerm, Data: map[string]any{"perm": acl.WRITE}})
|
||||
return
|
||||
}
|
||||
|
||||
if err = acl.UpdateResource(ctx, currentUser.GetUid(), old.GetResourceId(), map[string]string{"name": md.GetName()}); err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
md.SetId(id)
|
||||
|
||||
if err = mysql.DB.Transaction(func(tx *gorm.DB) (err error) {
|
||||
omits := []string{"resource_id", "created_at", "deleted_at"}
|
||||
switch t := any(md).(type) {
|
||||
case *model.Asset:
|
||||
if err = HandleAuthorization(currentUser, tx, model.ACTION_UPDATE, any(old).(*model.Asset), t); err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
}
|
||||
omits = append(omits, "ci_id")
|
||||
}
|
||||
|
||||
if err = mysql.DB.Omit(omits...).Save(md).Error; err != nil {
|
||||
return
|
||||
}
|
||||
err = mysql.DB.Create(&model.History{
|
||||
RemoteIp: ctx.ClientIP(),
|
||||
Type: md.TableName(),
|
||||
TargetId: md.GetId(),
|
||||
ActionType: model.ACTION_UPDATE,
|
||||
Old: toMap(old),
|
||||
New: toMap(md),
|
||||
CreatorId: currentUser.Uid,
|
||||
CreatedAt: time.Now(),
|
||||
}).Error
|
||||
return
|
||||
}); err != nil {
|
||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrDuplicateName, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, defaultHttpResponse)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func doGet[T any](ctx *gin.Context, needAcl bool, dbFind *gorm.DB, resourceType string, postHooks ...postHook[T]) (err error) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
|
||||
if needAcl && !acl.IsAdmin(currentUser) {
|
||||
//rs := make([]*acl.Resource, 0)
|
||||
var rs []*acl.Resource
|
||||
rs, err = acl.GetRoleResources(ctx, currentUser.Acl.Rid, resourceType)
|
||||
if err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
}
|
||||
dbFind = dbFind.Where("resource_id IN ?", lo.Map(rs, func(r *acl.Resource, _ int) int { return r.ResourceId }))
|
||||
}
|
||||
|
||||
dbCount := dbFind.Session(&gorm.Session{})
|
||||
dbFind = dbFind.Session(&gorm.Session{})
|
||||
|
||||
count, list := int64(0), make([]T, 0)
|
||||
eg := &errgroup.Group{}
|
||||
eg.Go(func() error {
|
||||
return dbCount.Count(&count).
|
||||
Error
|
||||
})
|
||||
eg.Go(func() error {
|
||||
pi, ps := cast.ToInt(ctx.Query("page_index")), cast.ToInt(ctx.Query("page_size"))
|
||||
if _, ok := ctx.GetQuery("page_index"); ok {
|
||||
dbFind = dbFind.Offset((pi - 1) * ps)
|
||||
}
|
||||
if _, ok := ctx.GetQuery("page_size"); ok {
|
||||
dbFind = dbFind.Limit(ps)
|
||||
}
|
||||
return dbFind.
|
||||
Order("id DESC").
|
||||
Find(&list).
|
||||
Error
|
||||
})
|
||||
if err = eg.Wait(); err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
for _, hook := range postHooks {
|
||||
if hook == nil {
|
||||
continue
|
||||
}
|
||||
hook(ctx, list)
|
||||
if ctx.IsAborted() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
res := &ListData{
|
||||
Count: count,
|
||||
List: lo.Map(list, func(t T, _ int) any { return t }),
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(res))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func handleRemoteErr(ctx *gin.Context, err error) {
|
||||
switch e := err.(type) {
|
||||
case *remote.RemoteError:
|
||||
if e.HttpCode == http.StatusBadRequest {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrRemoteClient, Data: e.Resp})
|
||||
return
|
||||
}
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrRemoteServer, Data: e.Resp})
|
||||
default:
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
}
|
||||
}
|
||||
|
||||
func filterSearch(ctx *gin.Context, db *gorm.DB, fields ...string) *gorm.DB {
|
||||
q, ok := ctx.GetQuery("search")
|
||||
if !ok || len(fields) <= 0 {
|
||||
return db
|
||||
}
|
||||
|
||||
d := mysql.DB
|
||||
for _, f := range fields {
|
||||
d = d.Or(fmt.Sprintf("%s LIKE ?", f), fmt.Sprintf("%%%s%%", q))
|
||||
}
|
||||
|
||||
db = db.Where(d)
|
||||
|
||||
return db
|
||||
}
|
||||
func filterStartEnd(ctx *gin.Context, db *gorm.DB, fields ...string) (*gorm.DB, error) {
|
||||
if q, ok := ctx.GetQuery("start"); ok {
|
||||
t, err := time.Parse(time.RFC3339, q)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, err)
|
||||
return db, err
|
||||
}
|
||||
db = db.Where("created_at >= ?", t)
|
||||
}
|
||||
if q, ok := ctx.GetQuery("end"); ok {
|
||||
t, err := time.Parse(time.RFC3339, q)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, err)
|
||||
return db, err
|
||||
}
|
||||
db = db.Where("created_at <= ?", t)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
func filterEqual(ctx *gin.Context, db *gorm.DB, fields ...string) *gorm.DB {
|
||||
for _, f := range fields {
|
||||
if q, ok := ctx.GetQuery(f); ok {
|
||||
db = db.Where(fmt.Sprintf("%s = ?", f), q)
|
||||
}
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
func filterLike(ctx *gin.Context, db *gorm.DB, fields ...string) *gorm.DB {
|
||||
likes := false
|
||||
d := mysql.DB
|
||||
for _, f := range fields {
|
||||
if q, ok := ctx.GetQuery(f); ok && q != "" {
|
||||
d = d.Or(fmt.Sprintf("%s LIKE ?", f), fmt.Sprintf("%%%s%%", q))
|
||||
likes = true
|
||||
}
|
||||
}
|
||||
if !likes {
|
||||
return db
|
||||
}
|
||||
db = db.Where(d)
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func toMap(data any) model.Map[string, any] {
|
||||
bs, _ := json.Marshal(data)
|
||||
res := make(map[string]any)
|
||||
json.Unmarshal(bs, &res)
|
||||
return res
|
||||
}
|
||||
|
||||
func getEmpty[T model.Model](data T) T {
|
||||
t := reflect.TypeOf(data).Elem()
|
||||
v := reflect.New(t)
|
||||
return v.Interface().(T)
|
||||
}
|
Reference in New Issue
Block a user