优化结构
This commit is contained in:
@@ -1,219 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"kra/internal/conf"
|
||||
"kra/internal/server/handler"
|
||||
"kra/internal/server/httpx"
|
||||
servermiddleware "kra/internal/server/middleware"
|
||||
serverrouter "kra/internal/server/router"
|
||||
"kra/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
kratoshttp "github.com/go-kratos/kratos/v3/transport/http"
|
||||
)
|
||||
|
||||
func NewGinEngine(runtime *conf.Runtime, access *service.AccessControlService, handlers *handler.Set, auth *service.AuthService, security *service.SecurityService, audit *service.AuditRecorder, logger *slog.Logger, version string) *gin.Engine {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
engine := gin.New()
|
||||
if err := engine.SetTrustedProxies(nil); err != nil && logger != nil {
|
||||
logger.Error("disable trusted proxies failed", "error", err)
|
||||
}
|
||||
engine.Use(servermiddleware.RequestMeta(), servermiddleware.Recovery(logger), servermiddleware.AccessLog(runtime, logger, version), servermiddleware.CORS(runtime), servermiddleware.ErrorAudit(logger), servermiddleware.SecurityRateLimit(security))
|
||||
|
||||
prefix := ""
|
||||
config := runtime.Admin()
|
||||
if config != nil {
|
||||
prefix = strings.TrimSuffix(config.RouterPrefix, "/")
|
||||
}
|
||||
public := engine.Group(prefix)
|
||||
public.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, "ok") })
|
||||
serverrouter.RegisterPublic(public, engine, handlers.Public)
|
||||
|
||||
private := engine.Group(prefix)
|
||||
// The reference administration behavior installs operation recording after JWT,
|
||||
// password and Casbin/data-scope middleware. Keeping the recorder after the
|
||||
// equivalent Kra guards avoids persisting rejected unauthenticated or
|
||||
// unauthorized requests as successful business operations.
|
||||
private.Use(servermiddleware.Auth(auth), servermiddleware.MustChangePassword(), servermiddleware.AccessControl(runtime, access), servermiddleware.OperationAudit(runtime, audit))
|
||||
serverrouter.RegisterUser(private, handlers.User)
|
||||
serverrouter.RegisterNavigation(private, handlers.Navigation)
|
||||
serverrouter.RegisterSession(private, handlers.Session)
|
||||
serverrouter.RegisterAuthority(private, handlers.Authority)
|
||||
serverrouter.RegisterMenu(private, handlers.Menu)
|
||||
serverrouter.RegisterAPI(private, public, engine, handlers.API)
|
||||
serverrouter.RegisterPermission(private, handlers.Permission)
|
||||
serverrouter.RegisterOrganization(private, handlers.Organization)
|
||||
serverrouter.RegisterDictionary(private, handlers.Dictionary)
|
||||
serverrouter.RegisterParameter(private, handlers.Parameter)
|
||||
serverrouter.RegisterAPIToken(private, handlers.APIToken)
|
||||
serverrouter.RegisterSystemConfig(private, handlers.SystemConfig)
|
||||
serverrouter.RegisterVersion(private, handlers.Version)
|
||||
serverrouter.RegisterExport(private, public, handlers.Export)
|
||||
serverrouter.RegisterAudit(private, public, handlers.Audit)
|
||||
serverrouter.RegisterTask(private, handlers.Task)
|
||||
serverrouter.RegisterMedia(private, handlers.Media)
|
||||
serverrouter.RegisterAnnouncement(private, public, handlers.Announcement)
|
||||
serverrouter.RegisterEmail(private, handlers.Email)
|
||||
serverrouter.RegisterPayment(private, public, handlers.Payment)
|
||||
registerSwagger(engine, prefix, version, logger)
|
||||
registerLocalStorage(engine, runtime)
|
||||
|
||||
engine.NoRoute(func(c *gin.Context) {
|
||||
if serveLocalStorage(c, runtime) {
|
||||
return
|
||||
}
|
||||
httpx.Fail(c, "请求的接口不存在")
|
||||
})
|
||||
logRegisteredRoutes(engine, logger)
|
||||
return engine
|
||||
}
|
||||
|
||||
func logRegisteredRoutes(engine *gin.Engine, logger *slog.Logger) {
|
||||
if logger == nil {
|
||||
return
|
||||
}
|
||||
routes := append([]gin.RouteInfo(nil), engine.Routes()...)
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
if routes[i].Path == routes[j].Path {
|
||||
return routes[i].Method < routes[j].Method
|
||||
}
|
||||
return routes[i].Path < routes[j].Path
|
||||
})
|
||||
systemLogger := logger.With("mod", "system")
|
||||
for _, route := range routes {
|
||||
systemLogger.Info("router registered", "method", route.Method, "path", route.Path)
|
||||
}
|
||||
systemLogger.Info("router register success", "route_count", len(routes))
|
||||
}
|
||||
|
||||
func NewGinServer(c *conf.Server, engine *gin.Engine) *kratoshttp.Server {
|
||||
network, address := "tcp", ":8000"
|
||||
if c != nil && c.Http != nil {
|
||||
if c.Http.Network != "" {
|
||||
network = c.Http.Network
|
||||
}
|
||||
if c.Http.Addr != "" {
|
||||
address = c.Http.Addr
|
||||
}
|
||||
}
|
||||
timeout := 10 * time.Minute
|
||||
if c != nil && c.Http != nil && c.Http.Timeout != nil && c.Http.Timeout.AsDuration() > 0 {
|
||||
timeout = c.Http.Timeout.AsDuration()
|
||||
}
|
||||
server := kratoshttp.NewServer(kratoshttp.Network(network), kratoshttp.Address(address), kratoshttp.Timeout(timeout))
|
||||
server.HandlePrefix("/", engine)
|
||||
server.Server.ReadHeaderTimeout = 10 * time.Second
|
||||
server.Server.ReadTimeout = timeout
|
||||
server.Server.WriteTimeout = timeout
|
||||
server.Server.IdleTimeout = 2 * time.Minute
|
||||
server.Server.MaxHeaderBytes = 1 << 20
|
||||
return server
|
||||
}
|
||||
|
||||
// serveLocalStorage resolves the local path for every request so a config
|
||||
// reload takes effect without rebuilding the Gin engine.
|
||||
func serveLocalStorage(c *gin.Context, runtime *conf.Runtime) bool {
|
||||
config := runtime.Admin()
|
||||
if config == nil || config.Local == nil || config.Local.StorePath == "" {
|
||||
return false
|
||||
}
|
||||
prefix := "/" + strings.Trim(config.Local.PathPrefix, "/")
|
||||
return serveLocalStorageAt(c, runtime, prefix)
|
||||
}
|
||||
|
||||
func registerLocalStorage(engine *gin.Engine, runtime *conf.Runtime) {
|
||||
config := runtime.Admin()
|
||||
if config == nil || config.Local == nil || config.Local.StorePath == "" || strings.Trim(config.Local.PathPrefix, "/") == "" {
|
||||
return
|
||||
}
|
||||
if config.Storage != nil && config.Storage.Type != "" && config.Storage.Type != "local" {
|
||||
return
|
||||
}
|
||||
prefix := "/" + strings.Trim(config.Local.PathPrefix, "/")
|
||||
if localStorageRouteConflicts(engine.Routes(), prefix) {
|
||||
return
|
||||
}
|
||||
handler := func(c *gin.Context) {
|
||||
if !serveLocalStorageAt(c, runtime, prefix) {
|
||||
c.Status(http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
engine.GET(prefix+"/*filepath", handler)
|
||||
engine.HEAD(prefix+"/*filepath", handler)
|
||||
}
|
||||
|
||||
func localStorageRouteConflicts(routes []gin.RouteInfo, prefix string) bool {
|
||||
staticRoot := strings.Split(strings.TrimPrefix(prefix, "/"), "/")[0]
|
||||
for _, route := range routes {
|
||||
if route.Method != http.MethodGet && route.Method != http.MethodHead {
|
||||
continue
|
||||
}
|
||||
routeRoot := strings.Split(strings.TrimPrefix(route.Path, "/"), "/")[0]
|
||||
if routeRoot == staticRoot {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func serveLocalStorageAt(c *gin.Context, runtime *conf.Runtime, prefix string) bool {
|
||||
config := runtime.Admin()
|
||||
if config == nil || config.Local == nil || config.Local.StorePath == "" {
|
||||
return false
|
||||
}
|
||||
if config.Storage != nil && config.Storage.Type != "" && config.Storage.Type != "local" {
|
||||
return false
|
||||
}
|
||||
if currentPrefix := "/" + strings.Trim(config.Local.PathPrefix, "/"); currentPrefix != prefix {
|
||||
return false
|
||||
}
|
||||
if prefix == "/" || (c.Request.URL.Path != prefix && !strings.HasPrefix(c.Request.URL.Path, prefix+"/")) {
|
||||
return false
|
||||
}
|
||||
c.Header("X-Content-Type-Options", "nosniff")
|
||||
filename := path.Base(c.Request.URL.Path)
|
||||
if !canServeUploadInline(filename) {
|
||||
c.Header("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filename}))
|
||||
}
|
||||
http.StripPrefix(prefix, http.FileServer(filesOnly{FileSystem: http.Dir(config.Local.StorePath)})).ServeHTTP(c.Writer, c.Request)
|
||||
return true
|
||||
}
|
||||
|
||||
func canServeUploadInline(filename string) bool {
|
||||
switch strings.ToLower(path.Ext(filename)) {
|
||||
case ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".ico", ".avif",
|
||||
".mp3", ".wav", ".ogg", ".m4a", ".flac", ".aac",
|
||||
".mp4", ".webm", ".mov", ".avi", ".mkv":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type filesOnly struct{ http.FileSystem }
|
||||
|
||||
func (f filesOnly) Open(name string) (http.File, error) {
|
||||
file, err := f.FileSystem.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, statErr := file.Stat()
|
||||
if statErr != nil {
|
||||
_ = file.Close()
|
||||
return nil, statErr
|
||||
}
|
||||
if info.IsDir() {
|
||||
_ = file.Close()
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
@@ -1,402 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"kra/internal/conf"
|
||||
"kra/internal/server/handler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func emptyHandlers() *handler.Set {
|
||||
return &handler.Set{
|
||||
Authority: &handler.Authority{}, Menu: &handler.Menu{}, API: &handler.API{},
|
||||
Permission: &handler.Permission{}, Organization: &handler.Organization{},
|
||||
Announcement: &handler.Announcement{}, Email: &handler.Email{}, Payment: &handler.Payment{}, Task: &handler.Task{},
|
||||
Media: &handler.Media{}, Audit: &handler.Audit{}, Export: &handler.Export{},
|
||||
Version: &handler.Version{}, Dictionary: &handler.Dictionary{}, Parameter: &handler.Parameter{},
|
||||
APIToken: &handler.APIToken{}, SystemConfig: &handler.SystemConfig{}, Public: &handler.Public{},
|
||||
User: &handler.User{}, Navigation: &handler.Navigation{}, Session: &handler.Session{},
|
||||
}
|
||||
}
|
||||
|
||||
func TestGinRouteContract(t *testing.T) {
|
||||
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
||||
routes := engine.Routes()
|
||||
actual := make([]string, 0, len(routes))
|
||||
seen := make(map[string]struct{}, len(routes))
|
||||
for _, route := range routes {
|
||||
key := route.Method + " " + route.Path
|
||||
if _, exists := seen[key]; exists {
|
||||
t.Fatalf("duplicate route %s", key)
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
actual = append(actual, key)
|
||||
}
|
||||
sort.Strings(actual)
|
||||
if value := strings.Join(actual, "\n"); value != expectedGinRouteContract {
|
||||
t.Fatalf("route contract changed:\n%s", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGinStartupLogsEveryRegisteredRoute(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&output, nil))
|
||||
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, logger, "test")
|
||||
text := output.String()
|
||||
if got, want := strings.Count(text, `"msg":"router registered"`), len(engine.Routes()); got != want {
|
||||
t.Fatalf("registered route log count = %d, want %d", got, want)
|
||||
}
|
||||
if !strings.Contains(text, `"msg":"router register success"`) || !strings.Contains(text, `"route_count":186`) {
|
||||
t.Fatalf("startup route summary is missing: %s", text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwaggerUsesRegisteredGinRoutes(t *testing.T) {
|
||||
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "v1.0.0")
|
||||
response := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, "/swagger/doc.json", nil)
|
||||
engine.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("swagger document status = %d, body=%s", response.Code, response.Body.String())
|
||||
}
|
||||
body := response.Body.String()
|
||||
for _, expected := range []string{`"swagger":"2.0"`, `"version":"v1.0.0"`, `"/base/login"`, `"/timedTask/triggerTimedTask"`, `"/mediaUpload/{uploadId}"`} {
|
||||
if !strings.Contains(body, expected) {
|
||||
t.Fatalf("swagger document missing %s", expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwaggerSupportsRouterPrefix(t *testing.T) {
|
||||
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{RouterPrefix: "/admin"}), nil, emptyHandlers(), nil, nil, nil, nil, "v1.0.0")
|
||||
for _, path := range []string{"/admin/swagger/index.html", "/admin/swagger/doc.json"} {
|
||||
response := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, path, nil)
|
||||
engine.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("swagger path %s status = %d, body=%s", path, response.Code, response.Body.String())
|
||||
}
|
||||
if strings.HasSuffix(path, "/doc.json") && !strings.Contains(response.Body.String(), `"basePath":"/admin"`) {
|
||||
t.Fatalf("swagger document does not use router prefix: %s", response.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwaggerUsesRootBasePathWithoutRouterPrefix(t *testing.T) {
|
||||
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "v1.0.0")
|
||||
response := httptest.NewRecorder()
|
||||
engine.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/swagger/doc.json", nil))
|
||||
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), `"basePath":"/"`) {
|
||||
t.Fatalf("swagger basePath mismatch: status=%d body=%s", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGinDoesNotTrustForwardedIPByDefault(t *testing.T) {
|
||||
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
||||
engine.GET("/client-ip", func(c *gin.Context) { c.String(http.StatusOK, c.ClientIP()) })
|
||||
request := httptest.NewRequest(http.MethodGet, "/client-ip", nil)
|
||||
request.RemoteAddr = "203.0.113.10:4321"
|
||||
request.Header.Set("X-Forwarded-For", "198.51.100.20")
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
engine.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusOK || response.Body.String() != "203.0.113.10" {
|
||||
t.Fatalf("client IP = %q, status=%d", response.Body.String(), response.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnnouncementDataSourceRequiresAuthentication(t *testing.T) {
|
||||
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
||||
response := httptest.NewRecorder()
|
||||
engine.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/info/getInfoDataSource", nil))
|
||||
if response.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalStorageResponseHeaders(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
for name, body := range map[string]string{"script.html": "<script>alert(1)</script>", "image.png": "png"} {
|
||||
if err := os.WriteFile(filepath.Join(root, name), []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: root, PathPrefix: "uploads/file"}, Storage: &conf.AdminBackend_Storage{Type: "local"}})
|
||||
engine := NewGinEngine(runtime, nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
||||
|
||||
for _, test := range []struct {
|
||||
path string
|
||||
attachment bool
|
||||
}{
|
||||
{path: "/uploads/file/script.html", attachment: true},
|
||||
{path: "/uploads/file/image.png", attachment: false},
|
||||
} {
|
||||
response := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, test.path, nil)
|
||||
engine.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusOK || response.Header().Get("X-Content-Type-Options") != "nosniff" {
|
||||
t.Fatalf("unexpected static response for %s: status=%d headers=%v", test.path, response.Code, response.Header())
|
||||
}
|
||||
hasAttachment := strings.Contains(response.Header().Get("Content-Disposition"), "attachment")
|
||||
if hasAttachment != test.attachment {
|
||||
t.Fatalf("attachment header for %s = %v, want %v", test.path, hasAttachment, test.attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalStorageRoutesAreRegistered(t *testing.T) {
|
||||
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: t.TempDir(), PathPrefix: "uploads/file"}})
|
||||
engine := NewGinEngine(runtime, nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
||||
seen := map[string]bool{}
|
||||
for _, route := range engine.Routes() {
|
||||
seen[route.Method+" "+route.Path] = true
|
||||
}
|
||||
for _, expected := range []string{"GET /uploads/file/*filepath", "HEAD /uploads/file/*filepath"} {
|
||||
if !seen[expected] {
|
||||
t.Fatalf("missing registered static route %s", expected)
|
||||
}
|
||||
}
|
||||
response := httptest.NewRecorder()
|
||||
engine.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/swagger/doc.json", nil))
|
||||
if strings.Contains(response.Body.String(), "/uploads/file/") {
|
||||
t.Fatal("swagger document should not expose static file wildcard")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalStorageOldPrefixStopsServingAfterReload(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(root, "image.png"), []byte("png"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: root, PathPrefix: "uploads/file"}, Storage: &conf.AdminBackend_Storage{Type: "local"}})
|
||||
engine := NewGinEngine(runtime, nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
||||
runtime.Replace(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: root, PathPrefix: "files"}, Storage: &conf.AdminBackend_Storage{Type: "local"}})
|
||||
|
||||
oldResponse := httptest.NewRecorder()
|
||||
engine.ServeHTTP(oldResponse, httptest.NewRequest(http.MethodGet, "/uploads/file/image.png", nil))
|
||||
if oldResponse.Code != http.StatusNotFound {
|
||||
t.Fatalf("old static prefix status = %d, want %d", oldResponse.Code, http.StatusNotFound)
|
||||
}
|
||||
newResponse := httptest.NewRecorder()
|
||||
engine.ServeHTTP(newResponse, httptest.NewRequest(http.MethodGet, "/files/image.png", nil))
|
||||
if newResponse.Code != http.StatusOK {
|
||||
t.Fatalf("new static prefix status = %d, want %d", newResponse.Code, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalStorageConflictingPrefixFallsBackWithoutStartupPanic(t *testing.T) {
|
||||
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: t.TempDir(), PathPrefix: "api"}, Storage: &conf.AdminBackend_Storage{Type: "local"}})
|
||||
engine := NewGinEngine(runtime, nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
||||
for _, route := range engine.Routes() {
|
||||
if route.Method == http.MethodGet && route.Path == "/api/*filepath" {
|
||||
t.Fatal("conflicting static wildcard must not be registered")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalStorageRouteIsNotRegisteredForRemoteStorage(t *testing.T) {
|
||||
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: t.TempDir(), PathPrefix: "uploads/file"}, Storage: &conf.AdminBackend_Storage{Type: "s3"}})
|
||||
engine := NewGinEngine(runtime, nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
||||
for _, route := range engine.Routes() {
|
||||
if strings.HasSuffix(route.Path, "/*filepath") && strings.HasPrefix(route.Path, "/uploads/file/") {
|
||||
t.Fatalf("remote storage registered local route %s %s", route.Method, route.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const expectedGinRouteContract = `DELETE /api/deleteApisByIds
|
||||
DELETE /dataAccessLog/deleteDataAccessLogByIds
|
||||
DELETE /department/deleteDepartment
|
||||
DELETE /info/deleteInfo
|
||||
DELETE /info/deleteInfoByIds
|
||||
DELETE /mediaUpload/:uploadId
|
||||
DELETE /position/deletePosition
|
||||
DELETE /sysDictionary/deleteSysDictionary
|
||||
DELETE /sysDictionaryDetail/deleteSysDictionaryDetail
|
||||
DELETE /sysError/deleteSysError
|
||||
DELETE /sysError/deleteSysErrorByIds
|
||||
DELETE /sysExportTemplate/deleteSysExportTemplate
|
||||
DELETE /sysExportTemplate/deleteSysExportTemplateByIds
|
||||
DELETE /sysLoginLog/deleteLoginLog
|
||||
DELETE /sysLoginLog/deleteLoginLogByIds
|
||||
DELETE /sysOperationRecord/deleteSysOperationRecord
|
||||
DELETE /sysOperationRecord/deleteSysOperationRecordByIds
|
||||
DELETE /sysParams/deleteSysParams
|
||||
DELETE /sysParams/deleteSysParamsByIds
|
||||
DELETE /sysVersion/deleteSysVersion
|
||||
DELETE /sysVersion/deleteSysVersionByIds
|
||||
DELETE /timedTask/deleteTimedTask
|
||||
DELETE /user/deleteUser
|
||||
GET /api/freshCasbin
|
||||
GET /api/getApiGroups
|
||||
GET /api/getApiRoles
|
||||
GET /api/syncApi
|
||||
GET /attachmentCategory/getCategoryList
|
||||
GET /authority/getDataScopeDepts
|
||||
GET /authority/getUsersByAuthority
|
||||
GET /department/findDepartment
|
||||
GET /department/getDepartmentUsers
|
||||
GET /fileUploadAndDownload/findFile
|
||||
GET /health
|
||||
GET /info/findInfo
|
||||
GET /info/getInfoDataSource
|
||||
GET /info/getInfoList
|
||||
GET /info/getInfoPublic
|
||||
GET /logViewer/content
|
||||
GET /logViewer/dates
|
||||
GET /logViewer/files
|
||||
GET /menu/getMenuRoles
|
||||
GET /payment/configs
|
||||
GET /payment/orders
|
||||
GET /position/findPosition
|
||||
GET /position/getPositionUsers
|
||||
GET /securityConfig/getSecurityConfig
|
||||
GET /swagger/*any
|
||||
GET /sysDictionary/exportSysDictionary
|
||||
GET /sysDictionary/findSysDictionary
|
||||
GET /sysDictionary/getSysDictionaryList
|
||||
GET /sysDictionary/getSysDictionaryListWithDetails
|
||||
GET /sysDictionaryDetail/findSysDictionaryDetail
|
||||
GET /sysDictionaryDetail/getDictionaryDetailsByParent
|
||||
GET /sysDictionaryDetail/getDictionaryPath
|
||||
GET /sysDictionaryDetail/getDictionaryTreeList
|
||||
GET /sysDictionaryDetail/getDictionaryTreeListByType
|
||||
GET /sysDictionaryDetail/getSysDictionaryDetailList
|
||||
GET /sysError/findSysError
|
||||
GET /sysError/getSysErrorList
|
||||
GET /sysExportTemplate/exportExcel
|
||||
GET /sysExportTemplate/exportExcelByToken
|
||||
GET /sysExportTemplate/exportTemplate
|
||||
GET /sysExportTemplate/exportTemplateByToken
|
||||
GET /sysExportTemplate/findSysExportTemplate
|
||||
GET /sysExportTemplate/getSysExportTemplateList
|
||||
GET /sysExportTemplate/previewSQL
|
||||
GET /sysLoginLog/findLoginLog
|
||||
GET /sysLoginLog/getLoginLogList
|
||||
GET /sysOperationRecord/findSysOperationRecord
|
||||
GET /sysOperationRecord/getSysOperationRecordList
|
||||
GET /sysParams/findSysParams
|
||||
GET /sysParams/getSysParam
|
||||
GET /sysParams/getSysParamsList
|
||||
GET /sysVersion/downloadVersionJson
|
||||
GET /sysVersion/findSysVersion
|
||||
GET /sysVersion/getSysVersionList
|
||||
GET /timedTask/alertStream
|
||||
GET /timedTask/getRegisteredMethods
|
||||
GET /timedTask/getTimedTaskList
|
||||
GET /timedTask/getTimedTaskLogList
|
||||
GET /user/getUserInfo
|
||||
POST /api/createApi
|
||||
POST /api/deleteApi
|
||||
POST /api/enterSyncApi
|
||||
POST /api/getAllApis
|
||||
POST /api/getApiById
|
||||
POST /api/getApiList
|
||||
POST /api/ignoreApi
|
||||
POST /api/setApiRoles
|
||||
POST /api/updateApi
|
||||
POST /attachmentCategory/addCategory
|
||||
POST /attachmentCategory/deleteCategory
|
||||
POST /authority/copyAuthority
|
||||
POST /authority/createAuthority
|
||||
POST /authority/deleteAuthority
|
||||
POST /authority/getAuthorityList
|
||||
POST /authority/setDataScope
|
||||
POST /authority/setRoleUsers
|
||||
POST /authorityBtn/canRemoveAuthorityBtn
|
||||
POST /authorityBtn/getAuthorityBtn
|
||||
POST /authorityBtn/setAuthorityBtn
|
||||
POST /base/captcha
|
||||
POST /base/login
|
||||
POST /casbin/getPolicyPathByAuthorityId
|
||||
POST /casbin/updateCasbin
|
||||
POST /dataAccessLog/getDataAccessLogList
|
||||
POST /department/createDepartment
|
||||
POST /department/getDepartmentList
|
||||
POST /department/setDepartmentUsers
|
||||
POST /email/emailTest
|
||||
POST /email/sendEmail
|
||||
POST /fileUploadAndDownload/deleteFile
|
||||
POST /fileUploadAndDownload/deleteFiles
|
||||
POST /fileUploadAndDownload/editFileName
|
||||
POST /fileUploadAndDownload/getFileList
|
||||
POST /fileUploadAndDownload/importURL
|
||||
POST /fileUploadAndDownload/listOssFiles
|
||||
POST /fileUploadAndDownload/upload
|
||||
POST /info/createInfo
|
||||
POST /init/checkdb
|
||||
POST /init/initdb
|
||||
POST /jwt/jsonInBlacklist
|
||||
POST /mediaUpload/chunk
|
||||
POST /mediaUpload/complete
|
||||
POST /mediaUpload/init
|
||||
POST /menu/addBaseMenu
|
||||
POST /menu/addMenuAuthority
|
||||
POST /menu/deleteBaseMenu
|
||||
POST /menu/getBaseMenuById
|
||||
POST /menu/getBaseMenuTree
|
||||
POST /menu/getMenu
|
||||
POST /menu/getMenuAuthority
|
||||
POST /menu/getMenuList
|
||||
POST /menu/setMenuRoles
|
||||
POST /menu/updateBaseMenu
|
||||
POST /payment/callback/:provider
|
||||
POST /payment/config
|
||||
POST /payment/create
|
||||
POST /payment/order
|
||||
POST /payment/query
|
||||
POST /payment/refund
|
||||
POST /position/createPosition
|
||||
POST /position/getPositionList
|
||||
POST /position/setPositionUsers
|
||||
POST /securityConfig/setSecurityConfig
|
||||
POST /sysApiToken/createApiToken
|
||||
POST /sysApiToken/deleteApiToken
|
||||
POST /sysApiToken/getApiTokenList
|
||||
POST /sysDictionary/createSysDictionary
|
||||
POST /sysDictionary/importSysDictionary
|
||||
POST /sysDictionaryDetail/createSysDictionaryDetail
|
||||
POST /sysError/createSysError
|
||||
POST /sysExportTemplate/createSysExportTemplate
|
||||
POST /sysExportTemplate/importExcel
|
||||
POST /sysParams/createSysParams
|
||||
POST /sysVersion/exportVersion
|
||||
POST /sysVersion/importVersion
|
||||
POST /system/getServerInfo
|
||||
POST /system/getSystemConfig
|
||||
POST /system/reloadSystem
|
||||
POST /system/setSystemConfig
|
||||
POST /timedTask/createTimedTask
|
||||
POST /timedTask/toggleTimedTask
|
||||
POST /timedTask/triggerTimedTask
|
||||
POST /user/admin_register
|
||||
POST /user/changePassword
|
||||
POST /user/getUserList
|
||||
POST /user/resetPassword
|
||||
POST /user/setUserAuthorities
|
||||
POST /user/setUserAuthority
|
||||
POST /user/setUserDepartments
|
||||
POST /user/setUserPositions
|
||||
PUT /authority/updateAuthority
|
||||
PUT /department/updateDepartment
|
||||
PUT /info/updateInfo
|
||||
PUT /position/updatePosition
|
||||
PUT /sysDictionary/updateSysDictionary
|
||||
PUT /sysDictionaryDetail/updateSysDictionaryDetail
|
||||
PUT /sysError/updateSysError
|
||||
PUT /sysExportTemplate/updateSysExportTemplate
|
||||
PUT /sysParams/updateSysParams
|
||||
PUT /timedTask/updateTimedTask
|
||||
PUT /user/setSelfInfo
|
||||
PUT /user/setSelfSetting
|
||||
PUT /user/setUserInfo`
|
||||
@@ -1,110 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Announcement struct{ service *service.AnnouncementService }
|
||||
|
||||
func NewAnnouncement(service *service.AnnouncementService) *Announcement {
|
||||
return &Announcement{service: service}
|
||||
}
|
||||
|
||||
func announcementInput(req dto.AnnouncementRequest) service.AnnouncementInput {
|
||||
return service.AnnouncementInput{ID: req.ID, Title: req.Title, Content: req.Content, UserID: req.UserID, Attachments: req.Attachments}
|
||||
}
|
||||
|
||||
func (h *Announcement) Create(c *gin.Context) {
|
||||
var req dto.AnnouncementRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.Create(c.Request.Context(), announcementInput(req)); err != nil {
|
||||
httpx.Fail(c, "创建失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建成功")
|
||||
}
|
||||
|
||||
func (h *Announcement) Delete(c *gin.Context) {
|
||||
if err := h.service.Delete(c.Request.Context(), c.Query("ID")); err != nil {
|
||||
httpx.Fail(c, "删除失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
|
||||
func (h *Announcement) DeleteByIDs(c *gin.Context) {
|
||||
if err := h.service.DeleteByIDs(c.Request.Context(), c.QueryArray("IDs[]")); err != nil {
|
||||
httpx.Fail(c, "批量删除失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
|
||||
}
|
||||
|
||||
func (h *Announcement) Update(c *gin.Context) {
|
||||
var req dto.AnnouncementRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.Update(c.Request.Context(), announcementInput(req)); err != nil {
|
||||
httpx.Fail(c, "更新失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
|
||||
}
|
||||
|
||||
func (h *Announcement) Find(c *gin.Context) {
|
||||
item, err := h.service.Find(c.Request.Context(), c.Query("ID"))
|
||||
if err != nil {
|
||||
httpx.Fail(c, "查询失败")
|
||||
return
|
||||
}
|
||||
httpx.OKWithData(c, item)
|
||||
}
|
||||
|
||||
func (h *Announcement) List(c *gin.Context) {
|
||||
var req dto.AnnouncementSearchRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.List(c.Request.Context(), req.Page, req.PageSize, req.StartCreatedAt, req.EndCreatedAt)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
|
||||
}
|
||||
|
||||
func (h *Announcement) DataSource(c *gin.Context) {
|
||||
users, err := h.service.UserOptions(c.Request.Context())
|
||||
if err != nil {
|
||||
httpx.Fail(c, "查询失败")
|
||||
return
|
||||
}
|
||||
httpx.OKWithData(c, gin.H{"userID": users})
|
||||
}
|
||||
|
||||
func (h *Announcement) Public(c *gin.Context) {
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"info": "不需要鉴权的公告接口信息"}, "获取成功")
|
||||
}
|
||||
|
||||
func parseTime(value string) (*time.Time, error) {
|
||||
if value == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parsed, err := time.Parse(time.RFC3339, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &parsed, nil
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type API struct{ service *service.APIService }
|
||||
|
||||
func NewAPI(service *service.APIService) *API { return &API{service: service} }
|
||||
|
||||
func (h *API) List(c *gin.Context) {
|
||||
var req dto.APIListRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.Page == 0 {
|
||||
httpx.Fail(c, "Page值不能为空")
|
||||
return
|
||||
}
|
||||
if req.PageSize == 0 {
|
||||
httpx.Fail(c, "PageSize值不能为空")
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.ListAPI(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
|
||||
}
|
||||
func (h *API) All(c *gin.Context) {
|
||||
items, err := h.service.AllAPI(c.Request.Context(), true)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"apis": items}, "获取成功")
|
||||
}
|
||||
func (h *API) Create(c *gin.Context) {
|
||||
var req dto.APIRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if message := validateAPIRequest(&req); message != "" {
|
||||
httpx.Fail(c, message)
|
||||
return
|
||||
}
|
||||
created, err := h.service.CreateAPIRequest(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "创建失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, created, "创建成功")
|
||||
}
|
||||
func (h *API) Update(c *gin.Context) {
|
||||
var req dto.APIRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if message := validateAPIRequest(&req); message != "" {
|
||||
httpx.Fail(c, message)
|
||||
return
|
||||
}
|
||||
if err := h.service.UpdateAPIRequest(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "修改失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "修改成功")
|
||||
}
|
||||
func (h *API) Delete(c *gin.Context) {
|
||||
var req dto.DeleteAPIRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "ID值不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteAPI(c.Request.Context(), req.ID); err != nil {
|
||||
httpx.Fail(c, "删除失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
func (h *API) DeleteByIDs(c *gin.Context) {
|
||||
var req dto.DeleteAPIsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteAPIs(c.Request.Context(), req.IDs); err != nil {
|
||||
httpx.Fail(c, "删除失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
func (h *API) Find(c *gin.Context) {
|
||||
var req dto.GetAPIRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "ID值不能为空")
|
||||
return
|
||||
}
|
||||
item, err := h.service.FindAPIResponse(c.Request.Context(), req.ID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"api": item}, "获取成功")
|
||||
}
|
||||
func (h *API) Groups(c *gin.Context) {
|
||||
groups, groupAPIMap, err := h.service.Groups(c.Request.Context())
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.OKWithData(c, gin.H{"groups": groups, "apiGroupMap": groupAPIMap})
|
||||
}
|
||||
func (h *API) Roles(c *gin.Context) {
|
||||
path, method := c.Query("path"), c.Query("method")
|
||||
if path == "" || method == "" {
|
||||
httpx.Fail(c, "API路径和请求方法不能为空")
|
||||
return
|
||||
}
|
||||
ids, err := h.service.APIRoleIDs(c.Request.Context(), path, method)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败"+err.Error())
|
||||
return
|
||||
}
|
||||
if ids == nil {
|
||||
ids = []uint{}
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, ids, "获取成功")
|
||||
}
|
||||
func (h *API) SetRoles(c *gin.Context) {
|
||||
var req dto.SetAPIRolesRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.Path == "" || req.Method == "" {
|
||||
httpx.Fail(c, "API路径和请求方法不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.SetAPIRoles(c.Request.Context(), req.Path, req.Method, req.AuthorityIDs); err != nil {
|
||||
httpx.Fail(c, "设置失败"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
|
||||
}
|
||||
func (h *API) Sync(engine *gin.Engine) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
routes := engine.Routes()
|
||||
values := make([]dto.APIRequest, 0, len(routes))
|
||||
for _, route := range routes {
|
||||
values = append(values, dto.APIRequest{Path: route.Path, Method: route.Method})
|
||||
}
|
||||
result, err := h.service.SyncAPIResponses(c.Request.Context(), values)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "同步失败")
|
||||
return
|
||||
}
|
||||
httpx.OKWithData(c, result)
|
||||
}
|
||||
}
|
||||
func (h *API) Ignore(c *gin.Context) {
|
||||
var req dto.IgnoreAPIRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.SetAPIIgnored(c.Request.Context(), req.Path, req.Method, req.Flag); err != nil {
|
||||
httpx.Fail(c, "忽略失败")
|
||||
return
|
||||
}
|
||||
httpx.OK(c)
|
||||
}
|
||||
func (h *API) ApplySync(c *gin.Context) {
|
||||
var req dto.ApplyAPISyncRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.ApplyAPISyncRequest(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "忽略失败")
|
||||
return
|
||||
}
|
||||
httpx.OK(c)
|
||||
}
|
||||
func (h *API) FreshCasbin(c *gin.Context) {
|
||||
if err := h.service.FreshCasbin(c.Request.Context()); err != nil {
|
||||
httpx.Fail(c, "刷新失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "刷新成功")
|
||||
}
|
||||
|
||||
func (h *API) SetPolicyPaths(c *gin.Context) {
|
||||
var req dto.SetPolicyPathsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.AuthorityID == 0 {
|
||||
httpx.Fail(c, "AuthorityId值不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.SetPolicyPathsRequest(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "更新失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
|
||||
}
|
||||
func (h *API) PolicyPaths(c *gin.Context) {
|
||||
var req dto.GetPolicyPathsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.AuthorityID == 0 {
|
||||
httpx.Fail(c, "AuthorityId值不能为空")
|
||||
return
|
||||
}
|
||||
paths, err := h.service.PolicyPathResponses(c.Request.Context(), req.AuthorityID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"paths": paths}, "获取成功")
|
||||
}
|
||||
|
||||
func validateAPIRequest(req *dto.APIRequest) string {
|
||||
switch {
|
||||
case req.Path == "":
|
||||
return "Path值不能为空"
|
||||
case req.Description == "":
|
||||
return "Description值不能为空"
|
||||
case req.APIGroup == "":
|
||||
return "ApiGroup值不能为空"
|
||||
case req.Method == "":
|
||||
return "Method值不能为空"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type APIToken struct {
|
||||
service *service.TokenService
|
||||
}
|
||||
|
||||
func NewAPIToken(service *service.TokenService) *APIToken {
|
||||
return &APIToken{service: service}
|
||||
}
|
||||
|
||||
func (h *APIToken) Create(c *gin.Context) {
|
||||
var req dto.CreateAPITokenRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
token, err := h.service.CreateAPIToken(c.Request.Context(), req.UserID, req.AuthorityID, req.Days, req.Remark)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "签发失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"token": token}, "签发成功")
|
||||
}
|
||||
|
||||
func (h *APIToken) List(c *gin.Context) {
|
||||
var req dto.APITokenListRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.APITokens(c.Request.Context(), req.Page, req.PageSize, req.UserID, req.Status)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
|
||||
}
|
||||
|
||||
func (h *APIToken) Delete(c *gin.Context) {
|
||||
var req dto.IDRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.DisableAPIToken(c.Request.Context(), req.ID); err != nil {
|
||||
httpx.Fail(c, "作废失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "作废成功")
|
||||
}
|
||||
@@ -1,304 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Audit struct {
|
||||
service *service.AuditService
|
||||
recorder *service.AuditRecorder
|
||||
logs *service.LogViewerService
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewAudit(service *service.AuditService, recorder *service.AuditRecorder, logs *service.LogViewerService, logger *slog.Logger) *Audit {
|
||||
return &Audit{service: service, recorder: recorder, logs: logs, logger: logger}
|
||||
}
|
||||
|
||||
func (h *Audit) Operations(c *gin.Context) {
|
||||
var req dto.OperationRecordSearchRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.OperationsFilter(c.Request.Context(), req.Page, req.PageSize, req.Path, req.Method, req.Status)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
|
||||
}
|
||||
func (h *Audit) Operation(c *gin.Context) {
|
||||
var req dto.AuditIDQuery
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "ID值不能为空")
|
||||
return
|
||||
}
|
||||
item, err := h.service.Operation(c.Request.Context(), req.ID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "查询失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"reSysOperationRecord": item}, "查询成功")
|
||||
}
|
||||
func (h *Audit) DeleteOperation(c *gin.Context) {
|
||||
var req dto.IDRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteOperations(c.Request.Context(), []int{int(req.ID)}); err != nil {
|
||||
httpx.Fail(c, "删除失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
func (h *Audit) DeleteOperations(c *gin.Context) {
|
||||
var req dto.AuditIDsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteOperations(c.Request.Context(), req.IDs); err != nil {
|
||||
httpx.Fail(c, "批量删除失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
|
||||
}
|
||||
|
||||
func (h *Audit) Logins(c *gin.Context) {
|
||||
var req dto.LoginLogSearchRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.LoginsFilter(c.Request.Context(), req.Page, req.PageSize, req.Username, req.Status)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
|
||||
}
|
||||
func (h *Audit) Login(c *gin.Context) {
|
||||
var req dto.AuditIDQuery
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
item, err := h.service.Login(c.Request.Context(), req.ID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "查询失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, item, "查询成功")
|
||||
}
|
||||
func (h *Audit) DeleteLogin(c *gin.Context) {
|
||||
var req dto.IDRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteLogins(c.Request.Context(), []int{int(req.ID)}); err != nil {
|
||||
httpx.Fail(c, "删除失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
func (h *Audit) DeleteLogins(c *gin.Context) {
|
||||
var req dto.AuditIDsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteLogins(c.Request.Context(), req.IDs); err != nil {
|
||||
httpx.Fail(c, "批量删除失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
|
||||
}
|
||||
|
||||
func (h *Audit) DataAccess(c *gin.Context) {
|
||||
var req dto.DataAccessListRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.DataAccessRequest(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
|
||||
}
|
||||
func (h *Audit) DeleteDataAccess(c *gin.Context) {
|
||||
var req dto.IDsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteDataAccess(c.Request.Context(), req.IDs); err != nil {
|
||||
httpx.Fail(c, "删除失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
|
||||
func (h *Audit) LogDates(c *gin.Context) {
|
||||
month := c.Query("month")
|
||||
// Month uses `binding:"required"`; an omitted or empty query is a
|
||||
// generic parameter error, not the service's format-specific error.
|
||||
if month == "" {
|
||||
httpx.Fail(c, "参数错误")
|
||||
return
|
||||
}
|
||||
data, err := h.logs.LogDates(c.Request.Context(), month)
|
||||
if err != nil {
|
||||
failLogViewer(c, err, h.logger)
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, data, "获取成功")
|
||||
}
|
||||
func (h *Audit) LogFiles(c *gin.Context) {
|
||||
date := c.Query("date")
|
||||
if date == "" {
|
||||
httpx.Fail(c, "参数错误")
|
||||
return
|
||||
}
|
||||
data, err := h.logs.LogFiles(c.Request.Context(), date)
|
||||
if err != nil {
|
||||
failLogViewer(c, err, h.logger)
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, data, "获取成功")
|
||||
}
|
||||
func (h *Audit) LogContent(c *gin.Context) {
|
||||
date, path := c.Query("date"), c.Query("path")
|
||||
if date == "" || path == "" {
|
||||
httpx.Fail(c, "参数错误")
|
||||
return
|
||||
}
|
||||
var cursor *int64
|
||||
if raw := c.Query("cursor"); raw != "" {
|
||||
value, err := strconv.ParseInt(raw, 10, 64)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
return
|
||||
}
|
||||
cursor = &value
|
||||
}
|
||||
// Preserve negative cursors for the usecase to classify with the compatible
|
||||
// "日志文件路径不合法" error rather than rejecting them as binding errors.
|
||||
data, err := h.logs.LogContent(c.Request.Context(), date, path, cursor)
|
||||
if err != nil {
|
||||
failLogViewer(c, err, h.logger)
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, data, "获取成功")
|
||||
}
|
||||
|
||||
func failLogViewer(c *gin.Context, err error, logger *slog.Logger) {
|
||||
if logger != nil {
|
||||
logger.ErrorContext(c.Request.Context(), "日志查看失败", "mod", "log-viewer", "error", err)
|
||||
}
|
||||
message := "读取日志失败"
|
||||
switch {
|
||||
case errors.Is(err, biz.ErrInvalidLogMonth):
|
||||
message = "日志月份格式不正确"
|
||||
case errors.Is(err, biz.ErrInvalidLogDate):
|
||||
message = "日志日期格式不正确"
|
||||
case errors.Is(err, biz.ErrInvalidLogPath):
|
||||
message = "日志文件路径不合法"
|
||||
case errors.Is(err, biz.ErrLogFileNotFound):
|
||||
message = "日志文件不存在"
|
||||
case errors.Is(err, biz.ErrLogFileUnreadable):
|
||||
message = "日志文件不可读取"
|
||||
case errors.Is(err, biz.ErrLogRootUnavailable):
|
||||
message = "日志目录不可读取"
|
||||
}
|
||||
httpx.Fail(c, message)
|
||||
}
|
||||
|
||||
func (h *Audit) DeleteError(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
|
||||
if err := h.service.DeleteErrors(c.Request.Context(), []uint{uint(id)}); err != nil {
|
||||
httpx.Fail(c, "删除失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
func (h *Audit) DeleteErrors(c *gin.Context) {
|
||||
if err := h.service.DeleteErrors(c.Request.Context(), IDsFromQuery(c)); err != nil {
|
||||
httpx.Fail(c, "批量删除失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
|
||||
}
|
||||
func (h *Audit) UpdateError(c *gin.Context) {
|
||||
var req dto.ErrorRecordMutationRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.UpdateErrorRequest(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "更新失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
|
||||
}
|
||||
func (h *Audit) Error(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
|
||||
item, err := h.service.Error(c.Request.Context(), uint(id))
|
||||
if err != nil {
|
||||
httpx.Fail(c, "查询失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OKWithData(c, item)
|
||||
}
|
||||
func (h *Audit) Errors(c *gin.Context) {
|
||||
p, size, err := page(c)
|
||||
if err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
createdAtRange := make([]time.Time, 0, 2)
|
||||
for _, raw := range c.QueryArray("createdAtRange[]") {
|
||||
value, parseErr := time.Parse(time.RFC3339, raw)
|
||||
if parseErr != nil {
|
||||
httpx.Fail(c, parseErr.Error())
|
||||
return
|
||||
}
|
||||
createdAtRange = append(createdAtRange, value)
|
||||
}
|
||||
items, total, err := h.service.ErrorsFilter(c.Request.Context(), p, size, c.Query("form"), c.Query("info"), createdAtRange)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: p, PageSize: size}, "获取成功")
|
||||
}
|
||||
func (h *Audit) CreateError(c *gin.Context) {
|
||||
var req dto.ErrorRecordMutationRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.recorder.CreateErrorMutationRequest(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "创建失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建成功")
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Authority struct{ service *service.AuthorityService }
|
||||
|
||||
func NewAuthority(service *service.AuthorityService) *Authority { return &Authority{service: service} }
|
||||
|
||||
func (h *Authority) List(c *gin.Context) {
|
||||
items, err := h.service.Authorities(c.Request.Context())
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, items, "获取成功")
|
||||
}
|
||||
func (h *Authority) Create(c *gin.Context) {
|
||||
var req dto.AuthorityRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.AuthorityID == 0 {
|
||||
httpx.Fail(c, "AuthorityId值不能为空")
|
||||
return
|
||||
}
|
||||
if req.AuthorityName == "" {
|
||||
httpx.Fail(c, "AuthorityName值不能为空")
|
||||
return
|
||||
}
|
||||
value, err := h.service.CreateAuthorityRequest(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "创建失败"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"authority": value}, "创建成功")
|
||||
}
|
||||
func (h *Authority) Copy(c *gin.Context) {
|
||||
var req dto.CopyAuthorityRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.OldAuthorityID == 0 {
|
||||
httpx.Fail(c, "OldAuthorityId值不能为空")
|
||||
return
|
||||
}
|
||||
if req.Authority.AuthorityID == 0 {
|
||||
httpx.Fail(c, "AuthorityId值不能为空")
|
||||
return
|
||||
}
|
||||
if req.Authority.AuthorityName == "" {
|
||||
httpx.Fail(c, "AuthorityName值不能为空")
|
||||
return
|
||||
}
|
||||
value, err := h.service.CopyAuthorityRequest(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "拷贝失败"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"authority": value}, "拷贝成功")
|
||||
}
|
||||
func (h *Authority) Update(c *gin.Context) {
|
||||
var req dto.AuthorityRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.AuthorityID == 0 {
|
||||
httpx.Fail(c, "AuthorityId值不能为空")
|
||||
return
|
||||
}
|
||||
if req.AuthorityName == "" {
|
||||
httpx.Fail(c, "AuthorityName值不能为空")
|
||||
return
|
||||
}
|
||||
value, err := h.service.UpdateAuthorityRequest(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "更新失败"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"authority": value}, "更新成功")
|
||||
}
|
||||
func (h *Authority) Delete(c *gin.Context) {
|
||||
var req dto.DeleteAuthorityRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.AuthorityID == 0 {
|
||||
httpx.Fail(c, "AuthorityId值不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteAuthority(c.Request.Context(), req.AuthorityID); err != nil {
|
||||
httpx.Fail(c, "删除失败"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
func (h *Authority) SetUsers(c *gin.Context) {
|
||||
var req dto.SetRoleUsersRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.AuthorityID == 0 {
|
||||
httpx.Fail(c, "角色ID不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.SetAuthorityUsers(c.Request.Context(), req.AuthorityID, req.UserIDs); err != nil {
|
||||
httpx.Fail(c, "设置失败"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
|
||||
}
|
||||
func (h *Authority) Users(c *gin.Context) {
|
||||
var req dto.SetRoleUsersRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
ids, err := h.service.AuthorityUserIDs(c.Request.Context(), req.AuthorityID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败"+err.Error())
|
||||
return
|
||||
}
|
||||
if ids == nil {
|
||||
ids = []uint{}
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, ids, "获取成功")
|
||||
}
|
||||
func (h *Authority) SetDataScope(c *gin.Context) {
|
||||
var req dto.SetDataScopeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.AuthorityID == 0 {
|
||||
httpx.Fail(c, "角色ID不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.SetDataScope(c.Request.Context(), req.AuthorityID, req.DataScope, req.DepartmentIDs); err != nil {
|
||||
httpx.Fail(c, "设置失败"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
|
||||
}
|
||||
func (h *Authority) DataScopeDepartments(c *gin.Context) {
|
||||
var req dto.SetDataScopeRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
ids, err := h.service.DataScopeDepartmentIDs(c.Request.Context(), req.AuthorityID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败"+err.Error())
|
||||
return
|
||||
}
|
||||
if ids == nil {
|
||||
ids = []uint{}
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, ids, "获取成功")
|
||||
}
|
||||
@@ -1,266 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Dictionary struct{ service *service.DictionaryService }
|
||||
|
||||
func NewDictionary(service *service.DictionaryService) *Dictionary {
|
||||
return &Dictionary{service: service}
|
||||
}
|
||||
|
||||
func (h *Dictionary) Create(c *gin.Context) {
|
||||
var req dto.DictionaryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
created, err := h.service.CreateDictionaryRequest(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "创建失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, created, "创建成功")
|
||||
}
|
||||
func (h *Dictionary) Update(c *gin.Context) {
|
||||
var req dto.DictionaryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.UpdateDictionaryRequest(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "更新失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
|
||||
}
|
||||
func (h *Dictionary) Delete(c *gin.Context) {
|
||||
var req dto.DictionaryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteDictionary(c.Request.Context(), req.ID); err != nil {
|
||||
httpx.Fail(c, "删除失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
func (h *Dictionary) Find(c *gin.Context) {
|
||||
var req dto.DictionaryRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
item, err := h.service.Dictionary(c.Request.Context(), req.ID, req.Type, req.Status, true)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "字典未创建或未开启")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"resysDictionary": item}, "查询成功")
|
||||
}
|
||||
func (h *Dictionary) List(details bool) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
items, _, err := h.service.Dictionaries(c.Request.Context(), 0, 0, c.Query("name"), "", details)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, items, "获取成功")
|
||||
}
|
||||
}
|
||||
func (h *Dictionary) Export(c *gin.Context) {
|
||||
var req dto.DictionaryRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "字典ID不能为空")
|
||||
return
|
||||
}
|
||||
item, err := h.service.ExportDictionary(c.Request.Context(), req.ID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "导出失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, item, "导出成功")
|
||||
}
|
||||
func (h *Dictionary) Import(c *gin.Context) {
|
||||
var req dto.ImportDictionaryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.ImportDictionaryJSON(c.Request.Context(), req.JSON); err != nil {
|
||||
httpx.Fail(c, "导入失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "导入成功")
|
||||
}
|
||||
|
||||
func (h *Dictionary) CreateDetail(c *gin.Context) {
|
||||
var req dto.DictionaryDetailRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.CreateDictionaryDetailRequest(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "创建失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建成功")
|
||||
}
|
||||
func (h *Dictionary) UpdateDetail(c *gin.Context) {
|
||||
var req dto.DictionaryDetailRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.UpdateDictionaryDetailRequest(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "更新失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
|
||||
}
|
||||
func (h *Dictionary) DeleteDetail(c *gin.Context) {
|
||||
var req dto.DictionaryDetailRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteDictionaryDetail(c.Request.Context(), req.ID); err != nil {
|
||||
httpx.Fail(c, "删除失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
func (h *Dictionary) FindDetail(c *gin.Context) {
|
||||
var req dto.DictionaryDetailRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "ID值不能为空")
|
||||
return
|
||||
}
|
||||
item, err := h.service.DictionaryDetail(c.Request.Context(), req.ID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "查询失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"reSysDictionaryDetail": item}, "查询成功")
|
||||
}
|
||||
func (h *Dictionary) Details(c *gin.Context) {
|
||||
p, size, err := page(c)
|
||||
if err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
var dictionaryID uint64
|
||||
if raw, exists := c.GetQuery("sysDictionaryID"); exists && raw != "" {
|
||||
var parseErr error
|
||||
dictionaryID, parseErr = strconv.ParseUint(raw, 10, 0)
|
||||
if parseErr != nil {
|
||||
httpx.Fail(c, parseErr.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
filter := dto.DictionaryDetailListRequest{DictionaryID: uint(dictionaryID), Label: c.Query("label"), Value: c.Query("value")}
|
||||
if raw, exists := c.GetQuery("status"); exists {
|
||||
value, parseErr := strconv.ParseBool(raw)
|
||||
if parseErr != nil {
|
||||
httpx.Fail(c, parseErr.Error())
|
||||
return
|
||||
}
|
||||
filter.Status = &value
|
||||
}
|
||||
if raw, exists := c.GetQuery("parentID"); exists {
|
||||
value, parseErr := strconv.ParseUint(raw, 10, 64)
|
||||
if parseErr != nil {
|
||||
httpx.Fail(c, parseErr.Error())
|
||||
return
|
||||
}
|
||||
parentID := uint(value)
|
||||
filter.ParentID = &parentID
|
||||
}
|
||||
if raw, exists := c.GetQuery("level"); exists {
|
||||
value, parseErr := strconv.Atoi(raw)
|
||||
if parseErr != nil {
|
||||
httpx.Fail(c, parseErr.Error())
|
||||
return
|
||||
}
|
||||
filter.Level = &value
|
||||
}
|
||||
items, total, err := h.service.DictionaryDetailsRequest(c.Request.Context(), p, size, filter)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: p, PageSize: size}, "获取成功")
|
||||
}
|
||||
func (h *Dictionary) Tree(byType bool) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
rawID := c.Query("sysDictionaryID")
|
||||
id, parseErr := strconv.ParseUint(rawID, 10, 32)
|
||||
typ := ""
|
||||
if byType {
|
||||
typ = c.Query("type")
|
||||
if typ == "" {
|
||||
httpx.Fail(c, "字典类型不能为空")
|
||||
return
|
||||
}
|
||||
} else if rawID == "" {
|
||||
httpx.Fail(c, "字典ID不能为空")
|
||||
return
|
||||
} else if parseErr != nil {
|
||||
httpx.Fail(c, "字典ID格式错误")
|
||||
return
|
||||
}
|
||||
items, err := h.service.DictionaryTree(c.Request.Context(), uint(id), typ)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"list": items}, "获取成功")
|
||||
}
|
||||
}
|
||||
func (h *Dictionary) DetailsByParent(c *gin.Context) {
|
||||
var req dto.DictionaryDetailsByParentRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
items, err := h.service.DictionaryDetailsByParent(c.Request.Context(), req.DictionaryID, req.ParentID, req.IncludeChildren)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"list": items}, "获取成功")
|
||||
}
|
||||
func (h *Dictionary) Path(c *gin.Context) {
|
||||
rawID := c.Query("id")
|
||||
if rawID == "" {
|
||||
httpx.Fail(c, "字典详情ID不能为空")
|
||||
return
|
||||
}
|
||||
id, parseErr := strconv.ParseUint(rawID, 10, 32)
|
||||
if parseErr != nil {
|
||||
httpx.Fail(c, "字典详情ID格式错误")
|
||||
return
|
||||
}
|
||||
path, err := h.service.DictionaryPath(c.Request.Context(), uint(id))
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"path": path}, "获取成功")
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Email struct{ service *service.EmailService }
|
||||
|
||||
func NewEmail(service *service.EmailService) *Email { return &Email{service: service} }
|
||||
|
||||
func (h *Email) Test(c *gin.Context) {
|
||||
if err := h.service.Test(c.Request.Context()); err != nil {
|
||||
httpx.Fail(c, "发送失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "发送成功")
|
||||
}
|
||||
|
||||
func (h *Email) Send(c *gin.Context) {
|
||||
var req dto.SendEmailRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.Send(c.Request.Context(), req.To, req.Subject, req.Body); err != nil {
|
||||
httpx.Fail(c, "发送失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "发送成功")
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Export struct {
|
||||
service *service.ExportService
|
||||
}
|
||||
|
||||
func NewExport(service *service.ExportService) *Export {
|
||||
return &Export{service: service}
|
||||
}
|
||||
|
||||
func exportParams(values url.Values) (map[string]string, error) {
|
||||
out := map[string]string{}
|
||||
nested, err := url.ParseQuery(values.Get("params"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for key, items := range nested {
|
||||
if len(items) > 0 {
|
||||
out[key] = items[0]
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func exportIssueParams(values url.Values, blank bool) (map[string]string, error) {
|
||||
params, err := exportParams(values)
|
||||
if err != nil && blank {
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
return params, err
|
||||
}
|
||||
|
||||
func (h *Export) Create(c *gin.Context) {
|
||||
var req dto.ExportTemplateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.Name == "" {
|
||||
httpx.Fail(c, "Name值不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.CreateRequest(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "创建失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建成功")
|
||||
}
|
||||
func (h *Export) Update(c *gin.Context) {
|
||||
var req dto.ExportTemplateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.Name == "" {
|
||||
httpx.Fail(c, "Name值不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.UpdateRequest(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "更新失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
|
||||
}
|
||||
func (h *Export) Delete(c *gin.Context) {
|
||||
var req dto.IDRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
// Deleting an empty model is rejected by GORM's global-update guard in the
|
||||
// reference endpoint. Preserve the same externally visible failure.
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "删除失败")
|
||||
return
|
||||
}
|
||||
if err := h.service.Delete(c.Request.Context(), []uint{req.ID}); err != nil {
|
||||
httpx.Fail(c, "删除失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
func (h *Export) DeleteMany(c *gin.Context) {
|
||||
var req dto.IDsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.Delete(c.Request.Context(), req.IDs); err != nil {
|
||||
httpx.Fail(c, "批量删除失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
|
||||
}
|
||||
func (h *Export) Find(c *gin.Context) {
|
||||
var req dto.ExportTemplateRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
item, err := h.service.Template(c.Request.Context(), req.ID, "")
|
||||
if err != nil {
|
||||
httpx.Fail(c, "查询失败")
|
||||
return
|
||||
}
|
||||
httpx.OKWithData(c, gin.H{"resysExportTemplate": item})
|
||||
}
|
||||
func (h *Export) List(c *gin.Context) {
|
||||
p, size, err := page(c)
|
||||
if err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
start, startErr := parseTime(c.Query("startCreatedAt"))
|
||||
end, endErr := parseTime(c.Query("endCreatedAt"))
|
||||
if startErr != nil || endErr != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.TemplatesFilter(c.Request.Context(), p, size, c.Query("name"), c.Query("tableName"), c.Query("templateID"), start, end)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: p, PageSize: size}, "获取成功")
|
||||
}
|
||||
func (h *Export) Preview(c *gin.Context) {
|
||||
templateID := c.Query("templateID")
|
||||
if templateID == "" {
|
||||
httpx.Fail(c, "模板ID不能为空")
|
||||
return
|
||||
}
|
||||
params, _ := exportParams(c.Request.URL.Query())
|
||||
sql, err := h.service.Preview(c.Request.Context(), templateID, params)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.OKWithData(c, gin.H{"sql": sql})
|
||||
}
|
||||
|
||||
func (h *Export) Issue(blank bool) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
templateID := c.Query("templateID")
|
||||
if templateID == "" {
|
||||
httpx.Fail(c, "模板ID不能为空")
|
||||
return
|
||||
}
|
||||
params, err := exportIssueParams(c.Request.URL.Query(), blank)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "解析 params 参数失败")
|
||||
return
|
||||
}
|
||||
token, err := h.service.IssueToken(c.Request.Context(), templateID, params, blank)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "导出令牌创建失败")
|
||||
return
|
||||
}
|
||||
path := "/sysExportTemplate/exportExcelByToken?token=" + token
|
||||
if blank {
|
||||
path = "/sysExportTemplate/exportTemplateByToken?token=" + token
|
||||
}
|
||||
httpx.OKWithData(c, path)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Export) Import(c *gin.Context) {
|
||||
templateID := c.Query("templateID")
|
||||
if templateID == "" {
|
||||
httpx.Fail(c, "模板ID不能为空")
|
||||
return
|
||||
}
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
httpx.Fail(c, "文件获取失败")
|
||||
return
|
||||
}
|
||||
opened, err := file.Open()
|
||||
if err != nil {
|
||||
httpx.Fail(c, "读取文件失败")
|
||||
return
|
||||
}
|
||||
defer opened.Close()
|
||||
if err = h.service.Import(c.Request.Context(), templateID, opened); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "导入成功")
|
||||
}
|
||||
|
||||
func (h *Export) Download(expectBlank bool) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
token := c.Query("token")
|
||||
if token == "" {
|
||||
httpx.Fail(c, "导出token不能为空")
|
||||
return
|
||||
}
|
||||
value, err := h.service.ConsumeToken(c.Request.Context(), token, expectBlank)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, service.ErrExportTokenMalformed):
|
||||
httpx.Fail(c, "解析导出参数失败")
|
||||
case errors.Is(err, service.ErrExportTokenType):
|
||||
httpx.Fail(c, "token类型错误")
|
||||
default:
|
||||
httpx.Fail(c, "导出token无效或已过期")
|
||||
}
|
||||
return
|
||||
}
|
||||
var data []byte
|
||||
var name string
|
||||
if expectBlank {
|
||||
data, name, err = h.service.ExportBlankTemplate(c.Request.Context(), value.TemplateID)
|
||||
} else {
|
||||
data, name, err = h.service.Export(c.Request.Context(), value.TemplateID, value.Params)
|
||||
}
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
if expectBlank {
|
||||
name = strings.TrimSuffix(name, "_template.xlsx") + "模板.xlsx"
|
||||
} else {
|
||||
name = strings.TrimSuffix(name, ".xlsx") + strings.ReplaceAll(uuid.NewString(), "-", "")[:6] + ".xlsx"
|
||||
}
|
||||
c.Header("Content-Disposition", "attachment; filename="+name)
|
||||
c.Header("success", "true")
|
||||
c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", data)
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExportParamsRejectsMalformedNestedQuery(t *testing.T) {
|
||||
_, err := exportParams(url.Values{"params": {"name=%zz"}})
|
||||
if err == nil {
|
||||
t.Fatal("exportParams accepted malformed nested query")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportParamsUsesFirstValue(t *testing.T) {
|
||||
got, err := exportParams(url.Values{"params": {"name=first&name=second"}})
|
||||
if err != nil {
|
||||
t.Fatalf("exportParams returned error: %v", err)
|
||||
}
|
||||
if got["name"] != "first" {
|
||||
t.Fatalf("exportParams name = %q, want first", got["name"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportIssueParamsKeepsBlankTemplateCompatible(t *testing.T) {
|
||||
got, err := exportIssueParams(url.Values{"params": {"name=%zz"}}, true)
|
||||
if err != nil {
|
||||
t.Fatalf("blank template params returned error: %v", err)
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("blank template params = %#v, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportIssueParamsRejectsMalformedExcelParams(t *testing.T) {
|
||||
_, err := exportIssueParams(url.Values{"params": {"name=%zz"}}, false)
|
||||
if err == nil {
|
||||
t.Fatal("Excel export accepted malformed nested query")
|
||||
}
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"kra/internal/server/httpx"
|
||||
servermiddleware "kra/internal/server/middleware"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Media struct{ service *service.MediaService }
|
||||
|
||||
func NewMedia(service *service.MediaService) *Media { return &Media{service: service} }
|
||||
|
||||
func (h *Media) limitMultipartBody(c *gin.Context) int64 {
|
||||
limit := h.service.MediaConfig().EffectiveMaxFileSize()
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, limit+(1<<20))
|
||||
return limit
|
||||
}
|
||||
|
||||
func rejectMediaTooLarge(c *gin.Context, err error, message string) bool {
|
||||
var tooLarge *http.MaxBytesError
|
||||
if !errors.As(err, &tooLarge) {
|
||||
return false
|
||||
}
|
||||
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, httpx.Response{Code: httpx.CodeError, Data: gin.H{}, Msg: message})
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *Media) Upload(c *gin.Context) {
|
||||
claims := servermiddleware.Claims(c)
|
||||
if claims == nil {
|
||||
httpx.Fail(c, "接收文件失败")
|
||||
return
|
||||
}
|
||||
limit := h.limitMultipartBody(c)
|
||||
header, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
if rejectMediaTooLarge(c, err, "文件超过大小上限") {
|
||||
return
|
||||
}
|
||||
httpx.Fail(c, "接收文件失败")
|
||||
return
|
||||
}
|
||||
if header.Size > limit {
|
||||
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, httpx.Response{Code: httpx.CodeError, Data: gin.H{}, Msg: "文件超过大小上限"})
|
||||
return
|
||||
}
|
||||
opened, err := header.Open()
|
||||
if err != nil {
|
||||
// Let the upload service surface this as the generic upload
|
||||
// failure; keep the endpoint message consistent.
|
||||
httpx.Fail(c, "上传文件失败")
|
||||
return
|
||||
}
|
||||
defer opened.Close()
|
||||
category, _ := strconv.Atoi(c.DefaultPostForm("classId", "0"))
|
||||
save := c.DefaultQuery("noSave", "0") == "0"
|
||||
item, err := h.service.Upload(c.Request.Context(), claims.ID, header.Filename, header.Header.Get("Content-Type"), category, opened, save)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "上传文件失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"file": item}, "上传成功")
|
||||
}
|
||||
|
||||
func (h *Media) List(c *gin.Context) {
|
||||
var req dto.MediaListRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.MediaListRequest(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
|
||||
}
|
||||
func (h *Media) Delete(c *gin.Context) {
|
||||
var req dto.IDRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.Delete(c.Request.Context(), req.ID); err != nil {
|
||||
httpx.Fail(c, "删除失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
func (h *Media) DeleteMany(c *gin.Context) {
|
||||
var req dto.MediaBatchDeleteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
failed := []uint{}
|
||||
for _, id := range req.IDs {
|
||||
if h.service.Delete(c.Request.Context(), id) != nil {
|
||||
failed = append(failed, id)
|
||||
}
|
||||
}
|
||||
if len(failed) > 0 {
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"failedIds": failed}, "部分文件删除失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
|
||||
}
|
||||
func (h *Media) Find(c *gin.Context) {
|
||||
id, parseErr := strconv.ParseUint(c.Query("id"), 10, 64)
|
||||
if parseErr != nil {
|
||||
httpx.Fail(c, "文件ID非法")
|
||||
return
|
||||
}
|
||||
item, err := h.service.Media(c.Request.Context(), uint(id))
|
||||
if err != nil {
|
||||
httpx.Fail(c, "查询失败")
|
||||
return
|
||||
}
|
||||
httpx.OKWithData(c, item)
|
||||
}
|
||||
func (h *Media) Rename(c *gin.Context) {
|
||||
var req dto.RenameMediaRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.Rename(c.Request.Context(), req.ID, req.Name); err != nil {
|
||||
httpx.Fail(c, "编辑失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "编辑成功")
|
||||
}
|
||||
func (h *Media) ImportURLs(c *gin.Context) {
|
||||
var req []dto.ImportMediaRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.ImportURLRequests(c.Request.Context(), req); err != nil {
|
||||
httpx.Fail(c, "导入URL失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "导入URL成功")
|
||||
}
|
||||
func (h *Media) Storage(c *gin.Context) {
|
||||
var req dto.StorageListRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
items, next, more, err := h.service.Storage(c.Request.Context(), req.Prefix, req.Cursor, req.Limit)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "列举存储桶文件失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"list": items, "nextCursor": next, "hasMore": more}, "获取成功")
|
||||
}
|
||||
|
||||
func (h *Media) Categories(c *gin.Context) {
|
||||
items, err := h.service.Categories(c.Request.Context())
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取分类列表失败")
|
||||
return
|
||||
}
|
||||
httpx.OKWithData(c, items)
|
||||
}
|
||||
func (h *Media) SaveCategory(c *gin.Context) {
|
||||
var req dto.CategoryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := h.service.SaveCategoryRequest(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "创建/更新失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建/更新成功")
|
||||
}
|
||||
func (h *Media) DeleteCategory(c *gin.Context) {
|
||||
var req dto.IDRequest
|
||||
if c.ShouldBindJSON(&req) != nil || req.ID == 0 {
|
||||
httpx.Fail(c, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteCategory(c.Request.Context(), req.ID); err != nil {
|
||||
httpx.Fail(c, "删除失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
|
||||
func (h *Media) InitUpload(c *gin.Context) {
|
||||
claims := servermiddleware.Claims(c)
|
||||
var req dto.InitUploadRequest
|
||||
if claims == nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if config := h.service.MediaConfig(); config.MaxFileSize > 0 && req.FileSize > config.MaxFileSize {
|
||||
httpx.Fail(c, "文件超过大小上限")
|
||||
return
|
||||
}
|
||||
result, err := h.service.InitUpload(c.Request.Context(), claims.ID, req.FileName, req.FileHash, req.FileSize, req.ChunkSize, req.ChunkTotal)
|
||||
if err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, result, "成功")
|
||||
}
|
||||
func (h *Media) SaveChunk(c *gin.Context) {
|
||||
claims := servermiddleware.Claims(c)
|
||||
if claims == nil {
|
||||
httpx.Fail(c, "接收分片失败")
|
||||
return
|
||||
}
|
||||
limit := h.limitMultipartBody(c)
|
||||
header, err := c.FormFile("chunk")
|
||||
if err != nil {
|
||||
if rejectMediaTooLarge(c, err, "分片超过大小上限") {
|
||||
return
|
||||
}
|
||||
httpx.Fail(c, "接收分片失败")
|
||||
return
|
||||
}
|
||||
if header.Size > limit {
|
||||
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, httpx.Response{Code: httpx.CodeError, Data: gin.H{}, Msg: "分片超过大小上限"})
|
||||
return
|
||||
}
|
||||
uploadID, parseErr := strconv.ParseUint(c.PostForm("uploadId"), 10, 64)
|
||||
if parseErr != nil || uploadID == 0 {
|
||||
httpx.Fail(c, "上传会话 ID 非法")
|
||||
return
|
||||
}
|
||||
index, parseErr := strconv.Atoi(c.PostForm("chunkIndex"))
|
||||
if parseErr != nil || index < 0 {
|
||||
httpx.Fail(c, "分片序号非法")
|
||||
return
|
||||
}
|
||||
opened, err := header.Open()
|
||||
if err != nil {
|
||||
httpx.Fail(c, "分片读取失败")
|
||||
return
|
||||
}
|
||||
defer opened.Close()
|
||||
if err = h.service.SaveChunk(c.Request.Context(), claims.ID, uint(uploadID), index, c.PostForm("chunkHash"), opened); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "ok")
|
||||
}
|
||||
func (h *Media) CompleteUpload(c *gin.Context) {
|
||||
claims := servermiddleware.Claims(c)
|
||||
var req dto.CompleteUploadRequest
|
||||
if claims == nil {
|
||||
httpx.Fail(c, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
item, err := h.service.CompleteUpload(c.Request.Context(), claims.ID, req.UploadID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"media": item}, "成功")
|
||||
}
|
||||
func (h *Media) CancelUpload(c *gin.Context) {
|
||||
claims := servermiddleware.Claims(c)
|
||||
id, _ := strconv.ParseUint(c.Param("uploadId"), 10, 64)
|
||||
if claims == nil {
|
||||
httpx.Fail(c, "未登录")
|
||||
return
|
||||
}
|
||||
if err := h.service.CancelUpload(c.Request.Context(), claims.ID, uint(id)); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "已取消")
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Menu struct{ service *service.MenuService }
|
||||
|
||||
func NewMenu(service *service.MenuService) *Menu { return &Menu{service: service} }
|
||||
|
||||
func (h *Menu) List(c *gin.Context) {
|
||||
items, err := h.service.Tree(c.Request.Context())
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, items, "获取成功")
|
||||
}
|
||||
|
||||
func (h *Menu) Tree(c *gin.Context) {
|
||||
items, err := h.service.Tree(c.Request.Context())
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"menus": items}, "获取成功")
|
||||
}
|
||||
|
||||
func (h *Menu) Create(c *gin.Context) {
|
||||
var req dto.MenuRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.Path == "" {
|
||||
httpx.Fail(c, "Path值不能为空")
|
||||
return
|
||||
}
|
||||
if req.Name == "" {
|
||||
httpx.Fail(c, "Name值不能为空")
|
||||
return
|
||||
}
|
||||
if req.Component == "" {
|
||||
httpx.Fail(c, "Component值不能为空")
|
||||
return
|
||||
}
|
||||
if req.Sort < 0 {
|
||||
httpx.Fail(c, "Sort长度或值不在合法范围,ge=0")
|
||||
return
|
||||
}
|
||||
if req.Meta.Title == "" {
|
||||
httpx.Fail(c, "Title值不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.Create(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "添加失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "添加成功")
|
||||
}
|
||||
|
||||
func (h *Menu) Update(c *gin.Context) {
|
||||
var req dto.MenuRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.Path == "" {
|
||||
httpx.Fail(c, "Path值不能为空")
|
||||
return
|
||||
}
|
||||
if req.Name == "" {
|
||||
httpx.Fail(c, "Name值不能为空")
|
||||
return
|
||||
}
|
||||
if req.Component == "" {
|
||||
httpx.Fail(c, "Component值不能为空")
|
||||
return
|
||||
}
|
||||
if req.Sort < 0 {
|
||||
httpx.Fail(c, "Sort长度或值不在合法范围,ge=0")
|
||||
return
|
||||
}
|
||||
if req.Meta.Title == "" {
|
||||
httpx.Fail(c, "Title值不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.Update(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "更新失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
|
||||
}
|
||||
|
||||
func (h *Menu) Delete(c *gin.Context) {
|
||||
var req dto.DeleteMenuRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "ID值不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.Delete(c.Request.Context(), uint(req.ID)); err != nil {
|
||||
httpx.Fail(c, "删除失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
|
||||
func (h *Menu) Find(c *gin.Context) {
|
||||
var req dto.GetMenuRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "ID值不能为空")
|
||||
return
|
||||
}
|
||||
item, err := h.service.Find(c.Request.Context(), uint(req.ID))
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"menu": item}, "获取成功")
|
||||
}
|
||||
|
||||
func (h *Menu) SetAuthorityMenus(c *gin.Context) {
|
||||
var req dto.SetAuthorityMenusRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.AuthorityID == 0 {
|
||||
httpx.Fail(c, "AuthorityId值不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.SetAuthorityMenus(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "添加失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "添加成功")
|
||||
}
|
||||
|
||||
func (h *Menu) AuthorityMenus(c *gin.Context) {
|
||||
var req dto.GetAuthorityMenusRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.AuthorityID == 0 {
|
||||
httpx.Fail(c, "AuthorityId值不能为空")
|
||||
return
|
||||
}
|
||||
menus, err := h.service.AuthorityMenus(c.Request.Context(), req.AuthorityID)
|
||||
if err != nil {
|
||||
httpx.Write(c, httpx.CodeError, gin.H{"menus": menus}, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"menus": menus}, "获取成功")
|
||||
}
|
||||
|
||||
func (h *Menu) RoleIDs(c *gin.Context) {
|
||||
var req dto.SetMenuRolesRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.MenuID == 0 {
|
||||
httpx.Fail(c, "菜单ID不能为空")
|
||||
return
|
||||
}
|
||||
ids, err := h.service.RoleIDs(c.Request.Context(), req.MenuID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败"+err.Error())
|
||||
return
|
||||
}
|
||||
if ids == nil {
|
||||
ids = []uint{}
|
||||
}
|
||||
defaultIDs, err := h.service.DefaultRouterRoleIDs(c.Request.Context(), req.MenuID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败"+err.Error())
|
||||
return
|
||||
}
|
||||
if defaultIDs == nil {
|
||||
defaultIDs = []uint{}
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"authorityIds": ids, "defaultRouterAuthorityIds": defaultIDs}, "获取成功")
|
||||
}
|
||||
|
||||
func (h *Menu) SetRoles(c *gin.Context) {
|
||||
var req dto.SetMenuRolesRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.MenuID == 0 {
|
||||
httpx.Fail(c, "菜单ID不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.SetRoles(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "设置失败"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/server/middleware"
|
||||
"kra/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Navigation struct{ service *service.UserService }
|
||||
|
||||
func NewNavigation(service *service.UserService) *Navigation { return &Navigation{service: service} }
|
||||
func (h *Navigation) Menu(c *gin.Context) {
|
||||
claims := middleware.Claims(c)
|
||||
if claims == nil {
|
||||
httpx.NoAuth(c, "未登录或非法访问")
|
||||
return
|
||||
}
|
||||
menus, err := h.service.Menus(c.Request.Context(), claims.AuthorityID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"menus": menus}, "获取成功")
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Organization struct {
|
||||
departments *service.DepartmentService
|
||||
positions *service.PositionService
|
||||
}
|
||||
|
||||
func NewOrganization(departments *service.DepartmentService, positions *service.PositionService) *Organization {
|
||||
return &Organization{departments: departments, positions: positions}
|
||||
}
|
||||
|
||||
func (h *Organization) ListDepartments(c *gin.Context) {
|
||||
var req dto.DepartmentListRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
items, err := h.departments.Departments(c.Request.Context(), req.Name)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, items, "获取成功")
|
||||
}
|
||||
func (h *Organization) CreateDepartment(c *gin.Context) {
|
||||
var req dto.DepartmentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.Name == "" {
|
||||
httpx.Fail(c, "部门名称不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.departments.Create(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "创建失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建成功")
|
||||
}
|
||||
func (h *Organization) UpdateDepartment(c *gin.Context) {
|
||||
var req dto.DepartmentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "部门ID不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.departments.Update(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "更新失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
|
||||
}
|
||||
func (h *Organization) DeleteDepartment(c *gin.Context) {
|
||||
var req dto.DeleteDepartmentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.departments.Delete(c.Request.Context(), req.ID); err != nil {
|
||||
httpx.Fail(c, "删除失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
func (h *Organization) FindDepartment(c *gin.Context) {
|
||||
var req dto.DeleteDepartmentRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
item, err := h.departments.Department(c.Request.Context(), req.ID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, item, "获取成功")
|
||||
}
|
||||
func (h *Organization) DepartmentUsers(c *gin.Context) {
|
||||
var req dto.SetDepartmentUsersRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
ids, err := h.departments.UserIDs(c.Request.Context(), req.DepartmentID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
if ids == nil {
|
||||
ids = []uint{}
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, ids, "获取成功")
|
||||
}
|
||||
func (h *Organization) SetDepartmentUsers(c *gin.Context) {
|
||||
var req dto.SetDepartmentUsersRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.DepartmentID == 0 {
|
||||
httpx.Fail(c, "部门ID不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.departments.SetUsers(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "设置失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
|
||||
}
|
||||
func (h *Organization) SetUserDepartments(c *gin.Context) {
|
||||
var req dto.SetUserDepartmentsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "用户ID不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.departments.SetUserDepartments(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "设置失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
|
||||
}
|
||||
|
||||
func (h *Organization) ListPositions(c *gin.Context) {
|
||||
var req dto.PositionListRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
items, total, err := h.positions.Positions(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
|
||||
}
|
||||
func (h *Organization) CreatePosition(c *gin.Context) {
|
||||
var req dto.PositionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.Name == "" {
|
||||
httpx.Fail(c, "岗位名称不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.positions.Create(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "创建失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建成功")
|
||||
}
|
||||
func (h *Organization) UpdatePosition(c *gin.Context) {
|
||||
var req dto.PositionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "岗位ID不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.positions.Update(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "更新失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
|
||||
}
|
||||
func (h *Organization) DeletePosition(c *gin.Context) {
|
||||
var req dto.DeletePositionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.positions.Delete(c.Request.Context(), req.ID); err != nil {
|
||||
httpx.Fail(c, "删除失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
func (h *Organization) FindPosition(c *gin.Context) {
|
||||
var req dto.DeletePositionRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
item, err := h.positions.Position(c.Request.Context(), req.ID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, item, "获取成功")
|
||||
}
|
||||
func (h *Organization) PositionUsers(c *gin.Context) {
|
||||
var req dto.SetPositionUsersRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
ids, err := h.positions.UserIDs(c.Request.Context(), req.PositionID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
if ids == nil {
|
||||
ids = []uint{}
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, ids, "获取成功")
|
||||
}
|
||||
func (h *Organization) SetPositionUsers(c *gin.Context) {
|
||||
var req dto.SetPositionUsersRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.PositionID == 0 {
|
||||
httpx.Fail(c, "岗位ID不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.positions.SetUsers(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "设置失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
|
||||
}
|
||||
func (h *Organization) SetUserPositions(c *gin.Context) {
|
||||
var req dto.SetUserPositionsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "用户ID不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.positions.SetUserPositions(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "设置失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Parameter struct {
|
||||
service *service.ParameterService
|
||||
}
|
||||
|
||||
func NewParameter(service *service.ParameterService) *Parameter {
|
||||
return &Parameter{service: service}
|
||||
}
|
||||
|
||||
func (h *Parameter) Create(c *gin.Context) {
|
||||
var req dto.SystemParameterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.CreateParameterRequest(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "创建失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建成功")
|
||||
}
|
||||
|
||||
func (h *Parameter) Update(c *gin.Context) {
|
||||
var req dto.SystemParameterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.UpdateParameterRequest(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "更新失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
|
||||
}
|
||||
|
||||
func (h *Parameter) Delete(c *gin.Context) {
|
||||
if err := h.service.DeleteParameters(c.Request.Context(), []string{c.Query("ID")}); err != nil {
|
||||
httpx.Fail(c, "删除失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
|
||||
func (h *Parameter) DeleteMany(c *gin.Context) {
|
||||
ids := c.QueryArray("IDs[]")
|
||||
if err := h.service.DeleteParameters(c.Request.Context(), ids); err != nil {
|
||||
httpx.Fail(c, "批量删除失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
|
||||
}
|
||||
|
||||
func (h *Parameter) Find(c *gin.Context) {
|
||||
item, err := h.service.ParameterByID(c.Request.Context(), c.Query("ID"))
|
||||
if err != nil {
|
||||
httpx.Fail(c, "查询失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OKWithData(c, item)
|
||||
}
|
||||
|
||||
func (h *Parameter) Get(c *gin.Context) {
|
||||
item, err := h.service.ParameterByKey(c.Request.Context(), c.Query("key"))
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, item, "获取成功")
|
||||
}
|
||||
|
||||
func (h *Parameter) List(c *gin.Context) {
|
||||
var req dto.SystemParameterSearchRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.ParametersFilter(c.Request.Context(), req.Page, req.PageSize, req.Name, req.Key, req.StartCreatedAt, req.EndCreatedAt)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Payment struct{ service *service.PaymentService }
|
||||
|
||||
func NewPayment(service *service.PaymentService) *Payment { return &Payment{service: service} }
|
||||
func (h *Payment) Configs(c *gin.Context) {
|
||||
v, err := h.service.Configs(c.Request.Context())
|
||||
if err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OKWithData(c, v)
|
||||
}
|
||||
func (h *Payment) SaveConfig(c *gin.Context) {
|
||||
var req dto.PaymentConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.SaveConfig(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OK(c)
|
||||
}
|
||||
func (h *Payment) Order(c *gin.Context) {
|
||||
var req dto.PaymentQueryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
order, err := h.service.Order(c.Request.Context(), req.Provider, req.TradeNo)
|
||||
if err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OKWithData(c, order)
|
||||
}
|
||||
|
||||
func (h *Payment) Orders(c *gin.Context) {
|
||||
var req dto.PaymentOrderListRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
items, total, page, pageSize, err := h.service.Orders(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: page, PageSize: pageSize}, "获取成功")
|
||||
}
|
||||
func (h *Payment) Create(c *gin.Context) {
|
||||
var req dto.PaymentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
req.ClientIP = c.ClientIP()
|
||||
result, err := h.service.Create(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OKWithData(c, result)
|
||||
}
|
||||
func (h *Payment) Query(c *gin.Context) {
|
||||
var req dto.PaymentQueryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
result, err := h.service.Query(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OKWithData(c, result)
|
||||
}
|
||||
func (h *Payment) Refund(c *gin.Context) {
|
||||
var req dto.PaymentRefundRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
result, err := h.service.Refund(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OKWithData(c, result)
|
||||
}
|
||||
func (h *Payment) Callback(c *gin.Context) {
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 8<<20)
|
||||
raw, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
writePaymentCallbackAck(c, service.DefaultPaymentCallbackAck(c.Param("provider"), false))
|
||||
return
|
||||
}
|
||||
headers := map[string]string{}
|
||||
for k, v := range c.Request.Header {
|
||||
if len(v) > 0 {
|
||||
headers[k] = v[0]
|
||||
}
|
||||
}
|
||||
query := map[string]string{}
|
||||
for k, v := range c.Request.URL.Query() {
|
||||
if len(v) > 0 {
|
||||
query[k] = v[0]
|
||||
}
|
||||
}
|
||||
ack, err := h.service.Callback(c.Request.Context(), &dto.PaymentCallbackRequest{Provider: c.Param("provider"), Headers: headers, Body: raw, Query: query})
|
||||
writePaymentCallbackAck(c, ack)
|
||||
_ = err
|
||||
}
|
||||
|
||||
func writePaymentCallbackAck(c *gin.Context, ack dto.PaymentCallbackAck) {
|
||||
if ack.StatusCode == 0 {
|
||||
ack.StatusCode = http.StatusInternalServerError
|
||||
}
|
||||
if ack.ContentType != "" {
|
||||
c.Header("Content-Type", ack.ContentType)
|
||||
}
|
||||
c.Status(ack.StatusCode)
|
||||
if len(ack.Body) > 0 {
|
||||
_, _ = c.Writer.Write(ack.Body)
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"kra/internal/service/dto"
|
||||
)
|
||||
|
||||
func TestWritePaymentCallbackAckDoesNotExposeError(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
recorder := httptest.NewRecorder()
|
||||
context, _ := gin.CreateTestContext(recorder)
|
||||
writePaymentCallbackAck(context, dto.PaymentCallbackAck{
|
||||
StatusCode: 500,
|
||||
ContentType: "text/plain; charset=utf-8",
|
||||
Body: []byte("failure"),
|
||||
})
|
||||
if recorder.Code != 500 {
|
||||
t.Fatalf("status = %d, want 500", recorder.Code)
|
||||
}
|
||||
if recorder.Body.String() != "failure" {
|
||||
t.Fatalf("body = %q, want fixed failure ack", recorder.Body.String())
|
||||
}
|
||||
if recorder.Body.String() == "internal order resolver details" {
|
||||
t.Fatal("internal error was exposed in callback response")
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Permission struct{ service *service.PermissionService }
|
||||
|
||||
func NewPermission(service *service.PermissionService) *Permission {
|
||||
return &Permission{service: service}
|
||||
}
|
||||
|
||||
func (h *Permission) Buttons(c *gin.Context) {
|
||||
var req dto.GetAuthorityButtonsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
selected, err := h.service.SelectedButtons(c.Request.Context(), req.AuthorityID, req.MenuID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "查询失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"selected": selected}, "查询成功")
|
||||
}
|
||||
|
||||
func (h *Permission) SetButtons(c *gin.Context) {
|
||||
var req dto.SetAuthorityButtonsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.SetSelectedButtons(c.Request.Context(), req.AuthorityID, req.MenuID, req.Selected); err != nil {
|
||||
httpx.Fail(c, "分配失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "分配成功")
|
||||
}
|
||||
|
||||
func (h *Permission) CanRemoveButton(c *gin.Context) {
|
||||
allowed, err := h.service.CanRemoveButton(c.Request.Context(), c.Query("id"))
|
||||
if err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if !allowed {
|
||||
httpx.Fail(c, "此按钮正在被使用无法删除")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mojocn/base64Captcha"
|
||||
)
|
||||
|
||||
type Public struct {
|
||||
auth *service.AuthService
|
||||
system *service.SystemConfigService
|
||||
settings *service.SecurityService
|
||||
store *captchaStore
|
||||
}
|
||||
|
||||
func NewPublic(auth *service.AuthService, system *service.SystemConfigService, settings *service.SecurityService) *Public {
|
||||
return &Public{auth: auth, system: system, settings: settings, store: &captchaStore{service: settings}}
|
||||
}
|
||||
|
||||
func (h *Public) captchaConfig() (int, int, int) {
|
||||
config := h.settings.CaptchaSettings()
|
||||
return config.KeyLong, config.ImageWidth, config.ImageHeight
|
||||
}
|
||||
|
||||
func (h *Public) Captcha(c *gin.Context) {
|
||||
keyLong, width, height := h.captchaConfig()
|
||||
security, err := h.settings.CurrentSecurity(c.Request.Context())
|
||||
if err != nil || security == nil {
|
||||
httpx.Fail(c, "安全服务暂不可用")
|
||||
return
|
||||
}
|
||||
openCaptcha := true
|
||||
if security != nil {
|
||||
keyLong, width, height = security.KeyLong, security.ImgWidth, security.ImgHeight
|
||||
ttl := time.Duration(security.CaptchaTimeout) * time.Second
|
||||
failures, counterErr := h.settings.EnsureLoginIPCounter(c.Request.Context(), c.ClientIP(), ttl)
|
||||
if counterErr != nil {
|
||||
httpx.Fail(c, "安全服务暂不可用")
|
||||
return
|
||||
}
|
||||
openCaptcha = security.CaptchaOpen == 0 || failures > security.CaptchaOpen
|
||||
}
|
||||
driver := base64Captcha.NewDriverDigit(height, width, keyLong, 0.7, 80)
|
||||
id, picture, _, err := base64Captcha.NewCaptcha(driver, h.store).Generate()
|
||||
if err != nil {
|
||||
httpx.Fail(c, "验证码获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"captchaId": id, "picPath": picture, "captchaLength": keyLong, "openCaptcha": openCaptcha}, "验证码获取成功")
|
||||
}
|
||||
|
||||
func (h *Public) Login(c *gin.Context) {
|
||||
var req dto.LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.Username == "" {
|
||||
httpx.Fail(c, "Username值不能为空")
|
||||
return
|
||||
}
|
||||
if req.Password == "" {
|
||||
httpx.Fail(c, "Password值不能为空")
|
||||
return
|
||||
}
|
||||
result, err := h.auth.Login(c.Request.Context(), &req, c.ClientIP(), c.Request.UserAgent())
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, biz.ErrAccountLocked):
|
||||
var locked *biz.AccountLockedError
|
||||
errors.As(err, &locked)
|
||||
minutes := 0
|
||||
if locked != nil {
|
||||
minutes = locked.Minutes
|
||||
}
|
||||
httpx.Fail(c, "账号已锁定,请 "+strconv.Itoa(minutes)+" 分钟后再试")
|
||||
case errors.Is(err, biz.ErrCaptchaInvalid):
|
||||
httpx.Fail(c, "验证码错误")
|
||||
case errors.Is(err, biz.ErrUserDisabled):
|
||||
httpx.Fail(c, "用户被禁止登录")
|
||||
case errors.Is(err, biz.ErrTokenIssue):
|
||||
httpx.Fail(c, "获取token失败")
|
||||
case errors.Is(err, biz.ErrLoginState):
|
||||
httpx.Fail(c, "设置登录状态失败")
|
||||
case errors.Is(err, biz.ErrTokenRevoke):
|
||||
httpx.Fail(c, "jwt作废失败")
|
||||
default:
|
||||
httpx.Fail(c, "用户名不存在或者密码错误")
|
||||
}
|
||||
return
|
||||
}
|
||||
maxAge := int(time.UnixMilli(result.ExpiresAt).Unix() - time.Now().Unix())
|
||||
httpx.SetTokenCookie(c, result.Token, maxAge)
|
||||
httpx.Write(c, httpx.CodeSuccess, result, "登录成功")
|
||||
}
|
||||
|
||||
func (h *Public) CheckDatabase(c *gin.Context) {
|
||||
initialized, err := h.system.IsInitialized(c.Request.Context())
|
||||
if err != nil {
|
||||
httpx.Fail(c, "数据库状态检查失败")
|
||||
return
|
||||
}
|
||||
message := "数据库无需初始化"
|
||||
if !initialized {
|
||||
message = "前往初始化数据库"
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"needInit": !initialized}, message)
|
||||
}
|
||||
|
||||
func (h *Public) InitializeDatabase(engine *gin.Engine) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
initialized, err := h.system.IsInitialized(c.Request.Context())
|
||||
if err != nil {
|
||||
httpx.Fail(c, "数据库状态检查失败")
|
||||
return
|
||||
}
|
||||
if initialized {
|
||||
httpx.Fail(c, "已存在数据库配置")
|
||||
return
|
||||
}
|
||||
var input dto.DatabaseInitRequest
|
||||
if c.ShouldBindJSON(&input) != nil {
|
||||
httpx.Fail(c, "参数校验不通过")
|
||||
return
|
||||
}
|
||||
routes := engine.Routes()
|
||||
values := make([]dto.Route, 0, len(routes))
|
||||
for _, route := range routes {
|
||||
values = append(values, dto.Route{Path: route.Path, Method: route.Method})
|
||||
}
|
||||
if err := h.system.InitializeRoutes(c.Request.Context(), &input, values); err != nil {
|
||||
httpx.Fail(c, "自动创建数据库失败,请查看后台日志,检查后在进行初始化")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "自动创建数据库成功")
|
||||
}
|
||||
}
|
||||
|
||||
type captchaStore struct {
|
||||
service *service.SecurityService
|
||||
}
|
||||
|
||||
func (s *captchaStore) Set(id, value string) error {
|
||||
expiration := s.service.CaptchaSettings().StoreExpiration
|
||||
return s.service.SetCaptcha(context.Background(), id, value, expiration)
|
||||
}
|
||||
func (s *captchaStore) Get(id string, clear bool) string {
|
||||
value, ok, err := s.service.GetCaptcha(context.Background(), id)
|
||||
if err != nil || !ok {
|
||||
return ""
|
||||
}
|
||||
if clear {
|
||||
_ = s.service.DeleteCaptcha(context.Background(), id)
|
||||
}
|
||||
return value
|
||||
}
|
||||
func (s *captchaStore) Verify(id, answer string, clear bool) bool {
|
||||
return id != "" && answer != "" && strings.EqualFold(s.Get(id, clear), answer)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func page(c *gin.Context) (int, int, error) {
|
||||
var value, size int
|
||||
if raw, exists := c.GetQuery("page"); exists && raw != "" {
|
||||
parsed, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
value = parsed
|
||||
}
|
||||
if raw, exists := c.GetQuery("pageSize"); exists && raw != "" {
|
||||
parsed, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
size = parsed
|
||||
}
|
||||
return value, size, nil
|
||||
}
|
||||
|
||||
func IDsFromQuery(c *gin.Context) []uint {
|
||||
values := c.QueryArray("IDs[]")
|
||||
ids := make([]uint, 0, len(values))
|
||||
for _, value := range values {
|
||||
id, _ := strconv.ParseUint(value, 10, 64)
|
||||
if id > 0 {
|
||||
ids = append(ids, uint(id))
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Session struct{ tokens *service.TokenService }
|
||||
|
||||
func NewSession(tokens *service.TokenService) *Session { return &Session{tokens: tokens} }
|
||||
func (h *Session) Logout(c *gin.Context) {
|
||||
token := c.GetHeader("x-token")
|
||||
if token == "" {
|
||||
token, _ = c.Cookie("x-token")
|
||||
}
|
||||
if err := h.tokens.BlacklistToken(c.Request.Context(), token); err != nil {
|
||||
httpx.Fail(c, "jwt作废失败")
|
||||
return
|
||||
}
|
||||
httpx.SetTokenCookie(c, "", -1)
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "jwt作废成功")
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package handler
|
||||
|
||||
type Set struct {
|
||||
Authority *Authority
|
||||
Menu *Menu
|
||||
API *API
|
||||
Permission *Permission
|
||||
Organization *Organization
|
||||
Announcement *Announcement
|
||||
Email *Email
|
||||
Payment *Payment
|
||||
Task *Task
|
||||
Media *Media
|
||||
Audit *Audit
|
||||
Export *Export
|
||||
Version *Version
|
||||
Dictionary *Dictionary
|
||||
Parameter *Parameter
|
||||
APIToken *APIToken
|
||||
SystemConfig *SystemConfig
|
||||
Public *Public
|
||||
User *User
|
||||
Navigation *Navigation
|
||||
Session *Session
|
||||
}
|
||||
|
||||
func NewSet(authority *Authority, menu *Menu, api *API, permission *Permission, organization *Organization, announcement *Announcement, email *Email, payment *Payment, task *Task, media *Media, audit *Audit, export *Export, version *Version, dictionary *Dictionary, parameter *Parameter, apiToken *APIToken, systemConfig *SystemConfig, public *Public, user *User, navigation *Navigation, session *Session) *Set {
|
||||
return &Set{Authority: authority, Menu: menu, API: api, Permission: permission, Organization: organization, Announcement: announcement, Email: email, Payment: payment, Task: task, Media: media, Audit: audit, Export: export, Version: version, Dictionary: dictionary, Parameter: parameter, APIToken: apiToken, SystemConfig: systemConfig, Public: public, User: user, Navigation: navigation, Session: session}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SystemConfig struct {
|
||||
system *service.SystemConfigService
|
||||
settings *service.SecurityService
|
||||
}
|
||||
|
||||
func NewSystemConfig(system *service.SystemConfigService, settings *service.SecurityService) *SystemConfig {
|
||||
return &SystemConfig{system: system, settings: settings}
|
||||
}
|
||||
|
||||
func (h *SystemConfig) GetSecurity(c *gin.Context) {
|
||||
value, err := h.settings.Security(c.Request.Context())
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取安全配置失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, value, "获取成功")
|
||||
}
|
||||
|
||||
func (h *SystemConfig) SetSecurity(c *gin.Context) {
|
||||
var req dto.SecurityConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
result, err := h.settings.SaveSecurityRequest(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "设置安全配置失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, result, "设置成功")
|
||||
}
|
||||
|
||||
func (h *SystemConfig) Get(c *gin.Context) {
|
||||
value, err := h.system.SystemConfig()
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, value, "获取成功")
|
||||
}
|
||||
|
||||
func (h *SystemConfig) Set(c *gin.Context) {
|
||||
var req dto.SetSystemConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.system.SaveSystemConfig(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "设置失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
|
||||
}
|
||||
|
||||
func (h *SystemConfig) Reload(c *gin.Context) {
|
||||
if err := h.system.ReloadConfig(c.Request.Context()); err != nil {
|
||||
httpx.Fail(c, "重载系统失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "重载系统成功")
|
||||
}
|
||||
|
||||
func (h *SystemConfig) ServerInfo(c *gin.Context) {
|
||||
server, err := h.system.ServerInfo()
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"server": server}, "获取成功")
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/server/middleware"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Task struct {
|
||||
service *service.TaskService
|
||||
}
|
||||
|
||||
func NewTask(service *service.TaskService) *Task {
|
||||
return &Task{service: service}
|
||||
}
|
||||
|
||||
func (h *Task) Create(c *gin.Context) {
|
||||
var req dto.TaskRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
_, err := h.service.CreateRequest(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "创建失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建成功")
|
||||
}
|
||||
|
||||
func (h *Task) Update(c *gin.Context) {
|
||||
var req dto.TaskRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.UpdateRequest(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "更新失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
|
||||
}
|
||||
|
||||
func (h *Task) Delete(c *gin.Context) {
|
||||
var req dto.DeleteTaskRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.Delete(c.Request.Context(), req.ID); err != nil {
|
||||
httpx.Fail(c, "删除失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
|
||||
func (h *Task) Toggle(c *gin.Context) {
|
||||
var req dto.ToggleTaskRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.Toggle(c.Request.Context(), req.ID, req.Enabled); err != nil {
|
||||
httpx.Fail(c, "操作失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "操作成功")
|
||||
}
|
||||
|
||||
func (h *Task) Trigger(c *gin.Context) {
|
||||
var req dto.DeleteTaskRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.Trigger(c.Request.Context(), req.ID); err != nil {
|
||||
httpx.Fail(c, "触发失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "已触发, 执行结果见执行日志")
|
||||
}
|
||||
|
||||
func (h *Task) List(c *gin.Context) {
|
||||
var req dto.TaskListRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.ListRequest(c.Request.Context(), req.Page, req.PageSize, req.Name, req.ExecutorType, req.Enabled)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
|
||||
}
|
||||
|
||||
func (h *Task) Logs(c *gin.Context) {
|
||||
var req dto.TaskLogListRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.Logs(c.Request.Context(), req.Page, req.PageSize, req.TaskID, req.Status)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
|
||||
}
|
||||
|
||||
func (h *Task) Methods(c *gin.Context) {
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"methods": h.service.RegisteredMethods()}, "获取成功")
|
||||
}
|
||||
|
||||
func (h *Task) AlertStream(c *gin.Context) {
|
||||
claims := middleware.Claims(c)
|
||||
if claims == nil || claims.ID == 0 {
|
||||
httpx.Fail(c, "未获取到用户身份")
|
||||
return
|
||||
}
|
||||
c.Header("Content-Type", "text/event-stream; charset=utf-8")
|
||||
c.Header("Cache-Control", "no-cache, no-transform")
|
||||
c.Header("Connection", "keep-alive")
|
||||
c.Header("X-Accel-Buffering", "no")
|
||||
flusher, ok := c.Writer.(http.Flusher)
|
||||
if !ok {
|
||||
httpx.Fail(c, "SSE 不受支持")
|
||||
return
|
||||
}
|
||||
// Remove the normal server deadline for long-lived SSE connections.
|
||||
controller := http.NewResponseController(c.Writer)
|
||||
_ = controller.SetWriteDeadline(time.Time{})
|
||||
_ = controller.SetReadDeadline(time.Time{})
|
||||
events := h.service.Subscribe(claims.ID)
|
||||
defer h.service.Unsubscribe(claims.ID, events)
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
_, _ = fmt.Fprint(c.Writer, ": connected\n\n")
|
||||
flusher.Flush()
|
||||
for {
|
||||
select {
|
||||
case event, open := <-events:
|
||||
if !open {
|
||||
return
|
||||
}
|
||||
_, _ = fmt.Fprintf(c.Writer, "event: timedTask:alert\ndata: %s\n\n", event)
|
||||
flusher.Flush()
|
||||
case <-ticker.C:
|
||||
_, _ = fmt.Fprint(c.Writer, ": ping\n\n")
|
||||
flusher.Flush()
|
||||
case <-c.Request.Context().Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,257 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/server/middleware"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
service *service.UserService
|
||||
auth *service.AuthService
|
||||
}
|
||||
|
||||
func NewUser(user *service.UserService, auth *service.AuthService) *User {
|
||||
return &User{service: user, auth: auth}
|
||||
}
|
||||
|
||||
func (h *User) List(c *gin.Context) {
|
||||
var req dto.UserListRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.Page == 0 {
|
||||
httpx.Fail(c, "Page值不能为空")
|
||||
return
|
||||
}
|
||||
if req.PageSize == 0 {
|
||||
httpx.Fail(c, "PageSize值不能为空")
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.ListUsersRequest(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
|
||||
}
|
||||
|
||||
func (h *User) Create(c *gin.Context) {
|
||||
var req dto.UserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.Username == "" {
|
||||
httpx.Fail(c, "Username值不能为空")
|
||||
return
|
||||
}
|
||||
if req.Password == "" {
|
||||
httpx.Fail(c, "Password值不能为空")
|
||||
return
|
||||
}
|
||||
if req.NickName == "" {
|
||||
httpx.Fail(c, "NickName值不能为空")
|
||||
return
|
||||
}
|
||||
if req.AuthorityID == 0 {
|
||||
httpx.Fail(c, "AuthorityId值不能为空")
|
||||
return
|
||||
}
|
||||
result, err := h.service.CreateUserRequest(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
if service.IsPasswordPolicyError(err) {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if result == nil {
|
||||
result = &dto.UserResponse{}
|
||||
}
|
||||
httpx.Write(c, httpx.CodeError, gin.H{"user": result}, "注册失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"user": result}, "注册成功")
|
||||
}
|
||||
|
||||
func (h *User) Update(c *gin.Context) {
|
||||
var req dto.UserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "ID值不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.UpdateUserRequest(c.Request.Context(), &req); err != nil {
|
||||
httpx.Fail(c, "修改失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
|
||||
}
|
||||
|
||||
func (h *User) UpdateSelf(c *gin.Context) {
|
||||
claims := middleware.Claims(c)
|
||||
if claims == nil {
|
||||
httpx.NoAuth(c, "未登录或非法访问")
|
||||
return
|
||||
}
|
||||
var req dto.SelfUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.UpdateSelfUserRequest(c.Request.Context(), claims.ID, &req); err != nil {
|
||||
httpx.Fail(c, "修改失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
|
||||
}
|
||||
|
||||
func (h *User) Delete(c *gin.Context) {
|
||||
var req dto.IDRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "ID值不能为空")
|
||||
return
|
||||
}
|
||||
claims := middleware.Claims(c)
|
||||
if claims != nil && claims.ID == req.ID {
|
||||
httpx.Fail(c, "删除失败, 无法删除自己。")
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteUser(c.Request.Context(), req.ID); err != nil {
|
||||
httpx.Fail(c, "删除失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
|
||||
func (h *User) ResetPassword(c *gin.Context) {
|
||||
var req dto.ResetPasswordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.ResetPassword(c.Request.Context(), req.ID, req.Password); err != nil {
|
||||
if service.IsPasswordPolicyError(err) {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Fail(c, "重置失败"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "重置成功")
|
||||
}
|
||||
|
||||
func (h *User) ChangePassword(c *gin.Context) {
|
||||
claims := middleware.Claims(c)
|
||||
var req dto.ChangePasswordRequest
|
||||
if claims == nil {
|
||||
httpx.NoAuth(c, "未登录或非法访问")
|
||||
return
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.Password == "" {
|
||||
httpx.Fail(c, "Password值不能为空")
|
||||
return
|
||||
}
|
||||
if req.NewPassword == "" {
|
||||
httpx.Fail(c, "NewPassword值不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.ChangePassword(c.Request.Context(), claims.ID, req.Password, req.NewPassword); err != nil {
|
||||
if service.IsPasswordPolicyError(err) {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Fail(c, "修改失败,原密码与当前账户不符")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "修改成功")
|
||||
}
|
||||
|
||||
func (h *User) SetSelfSetting(c *gin.Context) {
|
||||
claims := middleware.Claims(c)
|
||||
var setting map[string]any
|
||||
if claims == nil {
|
||||
httpx.NoAuth(c, "未登录或非法访问")
|
||||
return
|
||||
}
|
||||
if err := c.ShouldBindJSON(&setting); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.SetUserSetting(c.Request.Context(), claims.ID, setting); err != nil {
|
||||
httpx.Fail(c, "设置失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "设置成功")
|
||||
}
|
||||
|
||||
func (h *User) SetAuthorities(c *gin.Context) {
|
||||
var req dto.UserAuthoritiesRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.SetUserAuthorities(c.Request.Context(), req.ID, req.AuthorityIDs); err != nil {
|
||||
httpx.Fail(c, "修改失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "修改成功")
|
||||
}
|
||||
|
||||
func (h *User) SwitchAuthority(c *gin.Context) {
|
||||
claims := middleware.Claims(c)
|
||||
var req dto.SwitchAuthorityRequest
|
||||
if claims == nil {
|
||||
httpx.NoAuth(c, "未登录或非法访问")
|
||||
return
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.AuthorityID == 0 {
|
||||
httpx.Fail(c, "AuthorityId值不能为空")
|
||||
return
|
||||
}
|
||||
login, err := h.auth.SwitchAuthority(c.Request.Context(), claims, req.AuthorityID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
c.Header("new-token", login.Token)
|
||||
c.Header("new-expires-at", strconv.FormatInt(login.ExpiresAt/1000, 10))
|
||||
maxAge := int(time.UnixMilli(login.ExpiresAt).Unix() - time.Now().Unix())
|
||||
httpx.SetTokenCookie(c, login.Token, maxAge)
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "修改成功")
|
||||
}
|
||||
|
||||
func (h *User) Get(c *gin.Context) {
|
||||
claims := middleware.Claims(c)
|
||||
if claims == nil {
|
||||
httpx.NoAuth(c, "未登录或非法访问")
|
||||
return
|
||||
}
|
||||
result, err := h.service.UserByUUID(c.Request.Context(), claims.UUID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"userInfo": result}, "获取成功")
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Version struct{ service *service.VersionService }
|
||||
|
||||
func NewVersion(service *service.VersionService) *Version { return &Version{service: service} }
|
||||
|
||||
func versionStage(err error) (biz.VersionStage, bool) {
|
||||
var stageErr *biz.VersionStageError
|
||||
if !errors.As(err, &stageErr) {
|
||||
return "", false
|
||||
}
|
||||
return stageErr.Stage, true
|
||||
}
|
||||
|
||||
func (h *Version) Delete(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
|
||||
if err := h.service.DeleteVersions(c.Request.Context(), []uint{uint(id)}); err != nil {
|
||||
httpx.Fail(c, "删除失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "删除成功")
|
||||
}
|
||||
func (h *Version) DeleteMany(c *gin.Context) {
|
||||
raw := c.QueryArray("IDs[]")
|
||||
ids := make([]uint, 0, len(raw))
|
||||
for _, value := range raw {
|
||||
id, _ := strconv.ParseUint(value, 10, 64)
|
||||
ids = append(ids, uint(id))
|
||||
}
|
||||
if err := h.service.DeleteVersions(c.Request.Context(), ids); err != nil {
|
||||
httpx.Fail(c, "批量删除失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
|
||||
}
|
||||
func (h *Version) Find(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
|
||||
item, err := h.service.Version(c.Request.Context(), uint(id))
|
||||
if err != nil {
|
||||
httpx.Fail(c, "查询失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OKWithData(c, item)
|
||||
}
|
||||
func (h *Version) List(c *gin.Context) {
|
||||
var req dto.VersionSearchRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
name, code := "", ""
|
||||
if req.VersionName != nil {
|
||||
name = *req.VersionName
|
||||
}
|
||||
if req.VersionCode != nil {
|
||||
code = *req.VersionCode
|
||||
}
|
||||
createdAtRange := make([]*time.Time, 0, len(req.CreatedAtRange))
|
||||
for i := range req.CreatedAtRange {
|
||||
createdAtRange = append(createdAtRange, &req.CreatedAtRange[i])
|
||||
}
|
||||
items, total, err := h.service.Versions(c.Request.Context(), req.Page, req.PageSize, name, code, createdAtRange)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
|
||||
}
|
||||
|
||||
func (h *Version) Export(c *gin.Context) {
|
||||
var req dto.ExportVersionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.Export(c.Request.Context(), req.VersionName, req.VersionCode, req.Description, req.MenuIDs, req.APIIDs, req.DictIDs); err != nil {
|
||||
switch stage, _ := versionStage(err); stage {
|
||||
case biz.VersionStageMenus:
|
||||
httpx.Fail(c, "获取菜单数据失败:"+err.Error())
|
||||
case biz.VersionStageAPIs:
|
||||
httpx.Fail(c, "获取API数据失败:"+err.Error())
|
||||
case biz.VersionStageDictionaries:
|
||||
httpx.Fail(c, "获取字典数据失败:"+err.Error())
|
||||
case biz.VersionStageJSON:
|
||||
httpx.Fail(c, "JSON序列化失败:"+err.Error())
|
||||
case biz.VersionStageSave:
|
||||
httpx.Fail(c, "保存版本记录失败:"+err.Error())
|
||||
default:
|
||||
httpx.Fail(c, "创建发版失败:"+err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建发版成功")
|
||||
}
|
||||
func (h *Version) Download(c *gin.Context) {
|
||||
rawID := c.Query("ID")
|
||||
if rawID == "" {
|
||||
httpx.Fail(c, "版本ID不能为空")
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(rawID, 10, 64)
|
||||
raw, code, err := h.service.VersionData(c.Request.Context(), uint(id))
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取版本记录失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
name := fmt.Sprintf("version_%s_%s.json", code, time.Now().Format("20060102150405"))
|
||||
c.Header("Content-Type", "application/json")
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", name))
|
||||
c.Header("Content-Length", strconv.Itoa(len(raw)))
|
||||
c.Data(http.StatusOK, "application/json", []byte(raw))
|
||||
}
|
||||
func (h *Version) Import(c *gin.Context) {
|
||||
var req dto.ImportVersionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.Fail(c, "解析JSON数据失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.service.ImportRequest(c.Request.Context(), &req); err != nil {
|
||||
if errors.Is(err, biz.ErrInvalidVersion) {
|
||||
httpx.Fail(c, "版本信息格式错误")
|
||||
return
|
||||
}
|
||||
switch stage, _ := versionStage(err); stage {
|
||||
case biz.VersionStageMenus:
|
||||
httpx.Fail(c, "导入菜单失败: "+err.Error())
|
||||
case biz.VersionStageAPIs:
|
||||
httpx.Fail(c, "导入API失败: "+err.Error())
|
||||
case biz.VersionStageDictionaries:
|
||||
httpx.Fail(c, "导入字典失败: "+err.Error())
|
||||
default:
|
||||
httpx.Fail(c, "导入失败:"+err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "导入成功")
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func requestUsesHTTPS(request *http.Request) bool {
|
||||
if request.TLS != nil {
|
||||
return true
|
||||
}
|
||||
forwarded := strings.SplitN(request.Header.Get("X-Forwarded-Proto"), ",", 2)[0]
|
||||
return strings.EqualFold(strings.TrimSpace(forwarded), "https")
|
||||
}
|
||||
|
||||
// SetTokenCookie also marks cookies Secure behind TLS or a TLS-terminating
|
||||
// reverse proxy.
|
||||
func SetTokenCookie(c *gin.Context, value string, maxAge int) {
|
||||
cookie := &http.Cookie{Name: "x-token", Value: value, Path: "/", MaxAge: maxAge, Secure: requestUsesHTTPS(c.Request), HttpOnly: true, SameSite: http.SameSiteStrictMode}
|
||||
if maxAge < 0 {
|
||||
cookie.Expires = time.Unix(1, 0)
|
||||
}
|
||||
http.SetCookie(c.Writer, cookie)
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
CodeSuccess = 0
|
||||
CodeError = 7
|
||||
CodePasswordChangeRequired = 10001
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Data any `json:"data"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
type PageResult struct {
|
||||
List any `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
}
|
||||
|
||||
func Write(c *gin.Context, code int, data any, message string) {
|
||||
c.JSON(http.StatusOK, Response{Code: code, Data: data, Msg: message})
|
||||
}
|
||||
|
||||
func OK(c *gin.Context) { Write(c, CodeSuccess, gin.H{}, "操作成功") }
|
||||
|
||||
func OKWithData(c *gin.Context, data any) { Write(c, CodeSuccess, data, "成功") }
|
||||
|
||||
func Fail(c *gin.Context, message string) { Write(c, CodeError, gin.H{}, message) }
|
||||
|
||||
func NoAuth(c *gin.Context, message string) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, Response{Code: CodeError, Data: nil, Msg: message})
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/internal/conf"
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type accessController interface {
|
||||
Authorize(context.Context, uint, string, string) (bool, error)
|
||||
ContextWithDataScope(context.Context, uint, uint) (context.Context, error)
|
||||
}
|
||||
|
||||
func AccessControl(runtime *conf.Runtime, access accessController) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
claims := Claims(c)
|
||||
if claims == nil {
|
||||
httpx.NoAuth(c, "未登录或非法访问")
|
||||
return
|
||||
}
|
||||
path := c.Request.URL.Path
|
||||
policyPath := path
|
||||
if config := runtime.Admin(); config != nil {
|
||||
policyPath = service.NormalizeRoutePath(policyPath, config.RouterPrefix)
|
||||
}
|
||||
allowed, err := access.Authorize(c.Request.Context(), claims.AuthorityID, policyPath, c.Request.Method)
|
||||
if err != nil || !allowed {
|
||||
httpx.Write(c, httpx.CodeError, gin.H{}, "权限不足")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
requestContext, err := access.ContextWithDataScope(c.Request.Context(), claims.AuthorityID, claims.ID)
|
||||
if err != nil {
|
||||
httpx.Write(c, httpx.CodeError, gin.H{}, "数据权限解析失败")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if scope, ok := biz.DataScopeFromContext(requestContext); ok {
|
||||
requestID, _ := c.Get("request_id")
|
||||
scope.RequestID, _ = requestID.(string)
|
||||
scope.Method, scope.Path = c.Request.Method, path
|
||||
requestContext = biz.NewDataScopeContext(requestContext, scope)
|
||||
}
|
||||
requestContext = biz.NewActorContext(requestContext, biz.Actor{UserID: claims.ID, AuthorityID: claims.AuthorityID})
|
||||
c.Request = c.Request.WithContext(requestContext)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/internal/conf"
|
||||
"kra/internal/server/httpx"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const defaultRequestBodyLimit int64 = 8 << 20
|
||||
|
||||
// AccessLog is the single global request/response capture point, matching
|
||||
// the reference middleware ordering and making every HTTP request observable.
|
||||
func AccessLog(runtime *conf.Runtime, logger *slog.Logger, version string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
started := time.Now()
|
||||
var requestBody []byte
|
||||
multipart := strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data")
|
||||
mediaUpload := multipart && isMediaUploadRoute(c.FullPath())
|
||||
config := runtime.Admin()
|
||||
bodyLimit := defaultRequestBodyLimit
|
||||
if mediaUpload {
|
||||
bodyLimit = biz.DefaultMaxMediaFileSize + (1 << 20)
|
||||
if config != nil && config.Media != nil && config.Media.MaxFileSize > 0 {
|
||||
bodyLimit = config.Media.MaxFileSize + (1 << 20)
|
||||
}
|
||||
}
|
||||
bytesIn := c.Request.ContentLength
|
||||
maxBytes := 1 << 20
|
||||
writer := &captureWriter{ResponseWriter: c.Writer, maxBytes: maxBytes}
|
||||
c.Writer = writer
|
||||
c.Header("X-Kra-Version", version)
|
||||
requestReadFailed := c.Request.ContentLength > bodyLimit
|
||||
if requestReadFailed {
|
||||
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, gin.H{"code": httpx.CodeError, "msg": "请求体超过大小上限"})
|
||||
} else if c.Request.Body != nil && !mediaUpload {
|
||||
limited := http.MaxBytesReader(c.Writer, c.Request.Body, bodyLimit)
|
||||
var err error
|
||||
requestBody, err = io.ReadAll(limited)
|
||||
if err != nil {
|
||||
requestReadFailed = true
|
||||
var tooLarge *http.MaxBytesError
|
||||
if errors.As(err, &tooLarge) {
|
||||
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, gin.H{"code": httpx.CodeError, "msg": "请求体超过大小上限"})
|
||||
} else {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"code": httpx.CodeError, "msg": "请求体读取失败"})
|
||||
}
|
||||
} else {
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
|
||||
bytesIn = int64(len(requestBody))
|
||||
}
|
||||
} else if c.Request.Body != nil {
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, bodyLimit)
|
||||
}
|
||||
if bytesIn < 0 {
|
||||
bytesIn = 0
|
||||
}
|
||||
logLimit := 1024
|
||||
if config != nil && config.Zap != nil && config.Zap.AccessLogMaxBytes > 0 {
|
||||
logLimit = int(config.Zap.AccessLogMaxBytes)
|
||||
}
|
||||
requestText := ""
|
||||
if multipart {
|
||||
requestText = "[文件]"
|
||||
} else {
|
||||
requestText = redactJSON(requestBody, c.GetHeader("Content-Type"), logLimit)
|
||||
}
|
||||
c.Set(ctxReqBodyKey, requestText)
|
||||
c.Set(ctxRespBufferKey, &writer.body)
|
||||
if !requestReadFailed {
|
||||
c.Next()
|
||||
}
|
||||
if logger == nil {
|
||||
return
|
||||
}
|
||||
responseText := redactJSON(writer.body.Bytes(), c.Writer.Header().Get("Content-Type"), logLimit)
|
||||
userID, authorityID := uint(0), uint(0)
|
||||
if claims := Claims(c); claims != nil {
|
||||
userID, authorityID = claims.ID, claims.AuthorityID
|
||||
}
|
||||
route := c.FullPath()
|
||||
if route == "" {
|
||||
route = "unmatched"
|
||||
}
|
||||
bytesOut := int64(c.Writer.Size())
|
||||
if bytesOut < 0 {
|
||||
bytesOut = 0
|
||||
}
|
||||
privateErrors := strings.TrimRight(c.Errors.ByType(gin.ErrorTypePrivate).String(), "\n")
|
||||
attributes := []any{
|
||||
"mod", "http", "ip", c.ClientIP(), "method", c.Request.Method, "http_path", c.Request.URL.Path, "http_route", route,
|
||||
"http_status", c.Writer.Status(), "latency_ms", time.Since(started).Milliseconds(),
|
||||
"request_id", stringValueFromContext(c, "request_id"), "trace_id", stringValueFromContext(c, "trace_id"),
|
||||
"bytes_in", bytesIn, "bytes_out", bytesOut, "user_id", userID, "authority_id", authorityID,
|
||||
"error", c.Writer.Status() >= http.StatusInternalServerError || privateErrors != "", "ua", c.Request.UserAgent(), "req_query", c.Request.URL.RawQuery}
|
||||
if config != nil && config.Zap != nil && config.Zap.AccessReqHeaders {
|
||||
attributes = append(attributes, "req_headers", redactHeaders(c.Request.Header))
|
||||
}
|
||||
if config != nil && config.Zap != nil && config.Zap.AccessReqBody {
|
||||
attributes = append(attributes, "req_body", requestText)
|
||||
}
|
||||
if config != nil && config.Zap != nil && config.Zap.AccessRespData {
|
||||
attributes = append(attributes, "resp_data", responseText)
|
||||
}
|
||||
if privateErrors != "" {
|
||||
attributes = append(attributes, "error_msg", privateErrors)
|
||||
}
|
||||
logger.InfoContext(c.Request.Context(), "请求完成", attributes...)
|
||||
}
|
||||
}
|
||||
|
||||
func isMediaUploadRoute(route string) bool {
|
||||
return strings.HasSuffix(route, "/fileUploadAndDownload/upload") ||
|
||||
strings.HasSuffix(route, "/mediaUpload/chunk")
|
||||
}
|
||||
|
||||
func redactHeaders(headers map[string][]string) map[string]string {
|
||||
out := make(map[string]string, len(headers))
|
||||
for key, values := range headers {
|
||||
lower := strings.ToLower(key)
|
||||
if lower == "authorization" || lower == "cookie" || lower == "set-cookie" || lower == "x-token" {
|
||||
out[key] = "***"
|
||||
} else {
|
||||
out[key] = strings.Join(values, ",")
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func stringValueFromContext(c *gin.Context, key string) string {
|
||||
value, _ := c.Get(key)
|
||||
return stringValue(value)
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"kra/internal/conf"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestAccessLogRejectsOversizedRequestBody(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
called := false
|
||||
engine.Use(AccessLog(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, "test"))
|
||||
engine.POST("/payload", func(c *gin.Context) { called = true })
|
||||
request := httptest.NewRequest(http.MethodPost, "/payload", strings.NewReader(strings.Repeat("a", int(defaultRequestBodyLimit+1))))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
engine.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusRequestEntityTooLarge || called {
|
||||
t.Fatalf("status=%d called=%v body=%s", response.Code, called, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessLogRejectsOversizedMultipartOnOrdinaryRoute(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
called := false
|
||||
engine.Use(AccessLog(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, "test"))
|
||||
engine.POST("/login", func(c *gin.Context) { called = true })
|
||||
request := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(strings.Repeat("a", int(defaultRequestBodyLimit+1))))
|
||||
request.ContentLength = -1
|
||||
request.Header.Set("Content-Type", "multipart/form-data; boundary=test")
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
engine.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusRequestEntityTooLarge || called {
|
||||
t.Fatalf("status=%d called=%v body=%s", response.Code, called, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessLogAllowsMediaLimitOnlyOnUploadRoute(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
called := false
|
||||
engine.Use(AccessLog(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, "test"))
|
||||
engine.POST("/api/fileUploadAndDownload/upload", func(c *gin.Context) {
|
||||
called = true
|
||||
_, _ = io.Copy(io.Discard, c.Request.Body)
|
||||
})
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/fileUploadAndDownload/upload", strings.NewReader(strings.Repeat("a", int(defaultRequestBodyLimit+1))))
|
||||
request.Header.Set("Content-Type", "multipart/form-data; boundary=test")
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
engine.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusOK || !called {
|
||||
t.Fatalf("status=%d called=%v body=%s", response.Code, called, response.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/internal/conf"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type accessControllerStub struct {
|
||||
scopeErr error
|
||||
}
|
||||
|
||||
func (*accessControllerStub) Authorize(context.Context, uint, string, string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *accessControllerStub) ContextWithDataScope(ctx context.Context, authorityID, userID uint) (context.Context, error) {
|
||||
if s.scopeErr != nil {
|
||||
return ctx, s.scopeErr
|
||||
}
|
||||
return biz.NewDataScopeContext(ctx, biz.DataScope{UserID: userID, AuthorityID: authorityID, Scope: 1, All: true}), nil
|
||||
}
|
||||
|
||||
func TestAccessControlFailsClosedWhenDataScopeResolutionFails(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
engine.Use(func(c *gin.Context) {
|
||||
c.Set(claimsKey, &biz.AuthClaims{ID: 7, AuthorityID: 888})
|
||||
c.Next()
|
||||
})
|
||||
engine.Use(AccessControl(conf.NewRuntime(nil, &conf.AdminBackend{}), &accessControllerStub{scopeErr: errors.New("database unavailable")}))
|
||||
called := false
|
||||
engine.GET("/protected", func(c *gin.Context) { called = true })
|
||||
|
||||
response := httptest.NewRecorder()
|
||||
engine.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/protected", nil))
|
||||
if called {
|
||||
t.Fatal("protected handler ran after data-scope resolution failed")
|
||||
}
|
||||
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "数据权限解析失败") {
|
||||
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"kra/internal/conf"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const ctxOperationAuditPersistFailedKey = "operation_audit_persist_failed"
|
||||
|
||||
func OperationAudit(runtime *conf.Runtime, service *service.AuditRecorder) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
path := c.Request.URL.Path
|
||||
if !recordsOperation(c.Request.Method, path) {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
var requestBody []byte
|
||||
maxBytes := 1024
|
||||
if config := runtime.Admin(); config != nil && config.Zap != nil && config.Zap.AccessLogMaxBytes > 0 {
|
||||
maxBytes = int(config.Zap.AccessLogMaxBytes)
|
||||
}
|
||||
if c.Request.Method == http.MethodGet {
|
||||
requestBody = operationQueryBody(c.Request.URL.RawQuery)
|
||||
} else if value, ok := c.Get(ctxReqBodyKey); ok {
|
||||
requestBody = []byte(stringValue(value))
|
||||
} else if c.Request.Body != nil {
|
||||
// Fallback for tests or custom middleware chains that omit AccessLog.
|
||||
if strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data") {
|
||||
requestBody = []byte("[文件]")
|
||||
} else {
|
||||
limited := http.MaxBytesReader(c.Writer, c.Request.Body, defaultRequestBodyLimit)
|
||||
var err error
|
||||
requestBody, err = io.ReadAll(limited)
|
||||
if err != nil {
|
||||
var tooLarge *http.MaxBytesError
|
||||
if errors.As(err, &tooLarge) {
|
||||
c.AbortWithStatus(http.StatusRequestEntityTooLarge)
|
||||
} else {
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
}
|
||||
return
|
||||
}
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
|
||||
}
|
||||
}
|
||||
// The reference middleware captures up to the global 1 MiB response
|
||||
// safety limit and only applies the configured operation-log limit when a
|
||||
// download response is recorded. Using maxBytes here would silently
|
||||
// truncate ordinary JSON responses before that decision is possible.
|
||||
started := time.Now()
|
||||
c.Next()
|
||||
userID := uint(0)
|
||||
if claims := Claims(c); claims != nil {
|
||||
userID = claims.ID
|
||||
} else if value, err := strconv.ParseUint(c.GetHeader("x-user-id"), 10, 64); err == nil {
|
||||
userID = uint(value)
|
||||
}
|
||||
requestID, _ := c.Get("request_id")
|
||||
status := c.Writer.Status()
|
||||
responseBody := ""
|
||||
if value, ok := c.Get(ctxRespBufferKey); ok {
|
||||
if body, bok := value.(*bytes.Buffer); bok {
|
||||
responseBody = body.String()
|
||||
}
|
||||
}
|
||||
if isDownloadResponse(c) && len(responseBody) > maxBytes {
|
||||
responseBody = "[超出记录长度]"
|
||||
}
|
||||
errorMessage := c.Errors.ByType(gin.ErrorTypePrivate).String()
|
||||
if err := service.RecordOperationRequest(c.Request.Context(), &dto.OperationRecordRequest{IP: c.ClientIP(), Method: c.Request.Method, Path: path, Status: status, LatencyMS: time.Since(started).Milliseconds(), Agent: c.Request.UserAgent(), ErrorMessage: errorMessage, Body: operationRequestBody(requestBody, c.GetHeader("Content-Type"), maxBytes), Response: responseBody, UserID: userID, RequestID: stringValue(requestID), TraceID: stringValueFromContext(c, "trace_id"), DeviceID: c.GetHeader("X-Device-Id")}); err != nil {
|
||||
// Preserve the business response, but expose audit persistence failures
|
||||
// to the global access/error logging pipeline.
|
||||
c.Set(ctxOperationAuditPersistFailedKey, true)
|
||||
_ = c.Error(fmt.Errorf("operation audit persist: %w", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func operationQueryBody(raw string) []byte {
|
||||
query, _ := url.QueryUnescape(raw)
|
||||
values := make(map[string]string)
|
||||
for _, item := range strings.Split(query, "&") {
|
||||
parts := strings.Split(item, "=")
|
||||
if len(parts) == 2 {
|
||||
values[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
body, _ := json.Marshal(&values)
|
||||
return body
|
||||
}
|
||||
|
||||
func operationRequestBody(raw []byte, contentType string, limit int) string {
|
||||
if strings.Contains(contentType, "multipart/form-data") {
|
||||
return "[文件]"
|
||||
}
|
||||
text := string(raw)
|
||||
if strings.Contains(strings.ToLower(contentType), "json") && text != "" {
|
||||
var value any
|
||||
if json.Unmarshal(raw, &value) == nil {
|
||||
maskOperationBody(value)
|
||||
if encoded, err := json.Marshal(value); err == nil {
|
||||
text = string(encoded)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(text) > limit {
|
||||
return "[超出记录长度]"
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func maskOperationBody(value any) {
|
||||
switch current := value.(type) {
|
||||
case map[string]any:
|
||||
for key, item := range current {
|
||||
normalized := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(key, "_", ""), "-", ""))
|
||||
if normalized == "password" || normalized == "newpassword" || normalized == "oldpassword" || normalized == "confirmpassword" || normalized == "passwd" || normalized == "pwd" || normalized == "token" || normalized == "accesstoken" || normalized == "refreshtoken" || normalized == "secret" || normalized == "clientsecret" || normalized == "apikey" || normalized == "privatekey" || normalized == "idcard" {
|
||||
current[key] = "***"
|
||||
continue
|
||||
}
|
||||
maskOperationBody(item)
|
||||
}
|
||||
case []any:
|
||||
for _, item := range current {
|
||||
maskOperationBody(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isDownloadResponse(c *gin.Context) bool {
|
||||
header := c.Writer.Header()
|
||||
return strings.Contains(header.Get("Pragma"), "public") ||
|
||||
strings.Contains(header.Get("Expires"), "0") ||
|
||||
strings.Contains(header.Get("Cache-Control"), "must-revalidate, post-check=0, pre-check=0") ||
|
||||
strings.Contains(header.Get("Content-Type"), "application/force-download") ||
|
||||
strings.Contains(header.Get("Content-Type"), "application/octet-stream") ||
|
||||
strings.Contains(header.Get("Content-Type"), "application/vnd.ms-excel") ||
|
||||
strings.Contains(header.Get("Content-Type"), "application/download") ||
|
||||
strings.Contains(header.Get("Content-Disposition"), "attachment") ||
|
||||
strings.Contains(header.Get("Content-Transfer-Encoding"), "binary")
|
||||
}
|
||||
|
||||
// recordsOperation mirrors the routes on which operation records are enabled.
|
||||
// Matching by suffix keeps the behavior stable when router-prefix is configured.
|
||||
func recordsOperation(method, path string) bool {
|
||||
_, ok := operationRoutes[method+" "+routeSuffix(path)]
|
||||
return ok
|
||||
}
|
||||
|
||||
func routeSuffix(path string) string {
|
||||
for _, marker := range []string{"/user/", "/api/", "/casbin/", "/authority/", "/menu/", "/department/", "/position/", "/sysDictionary/", "/sysDictionaryDetail/", "/sysParams/", "/securityConfig/", "/system/", "/sysApiToken/", "/sysVersion/", "/sysExportTemplate/", "/sysError/", "/sysLoginLog/", "/sysOperationRecord/", "/dataAccessLog/", "/timedTask/", "/info/", "/email/"} {
|
||||
if index := strings.Index(path, marker); index >= 0 {
|
||||
return path[index:]
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
var operationRoutes = func() map[string]struct{} {
|
||||
values := []string{
|
||||
"POST /user/admin_register", "POST /user/changePassword", "POST /user/setUserAuthority", "DELETE /user/deleteUser", "PUT /user/setUserInfo", "PUT /user/setSelfInfo", "POST /user/setUserAuthorities", "POST /user/setUserDepartments", "POST /user/setUserPositions", "POST /user/resetPassword", "PUT /user/setSelfSetting",
|
||||
"GET /api/getApiGroups", "GET /api/syncApi", "POST /api/ignoreApi", "POST /api/enterSyncApi", "POST /api/createApi", "POST /api/deleteApi", "POST /api/getApiById", "POST /api/updateApi", "DELETE /api/deleteApisByIds", "POST /api/setApiRoles", "POST /casbin/updateCasbin",
|
||||
"POST /authority/createAuthority", "POST /authority/deleteAuthority", "PUT /authority/updateAuthority", "POST /authority/copyAuthority", "POST /authority/setDataScope", "POST /authority/setRoleUsers",
|
||||
"POST /menu/addBaseMenu", "POST /menu/addMenuAuthority", "POST /menu/deleteBaseMenu", "POST /menu/updateBaseMenu", "POST /menu/setMenuRoles",
|
||||
"POST /department/createDepartment", "PUT /department/updateDepartment", "DELETE /department/deleteDepartment", "POST /department/setDepartmentUsers",
|
||||
"POST /position/createPosition", "PUT /position/updatePosition", "DELETE /position/deletePosition", "POST /position/setPositionUsers",
|
||||
"POST /sysDictionary/createSysDictionary", "DELETE /sysDictionary/deleteSysDictionary", "PUT /sysDictionary/updateSysDictionary", "POST /sysDictionary/importSysDictionary", "GET /sysDictionary/exportSysDictionary",
|
||||
"POST /sysDictionaryDetail/createSysDictionaryDetail", "DELETE /sysDictionaryDetail/deleteSysDictionaryDetail", "PUT /sysDictionaryDetail/updateSysDictionaryDetail",
|
||||
"POST /sysParams/createSysParams", "DELETE /sysParams/deleteSysParams", "DELETE /sysParams/deleteSysParamsByIds", "PUT /sysParams/updateSysParams",
|
||||
"POST /securityConfig/setSecurityConfig", "POST /system/setSystemConfig", "POST /system/reloadSystem",
|
||||
"POST /sysApiToken/createApiToken", "POST /sysApiToken/getApiTokenList", "POST /sysApiToken/deleteApiToken",
|
||||
"DELETE /sysVersion/deleteSysVersion", "DELETE /sysVersion/deleteSysVersionByIds", "POST /sysVersion/exportVersion", "POST /sysVersion/importVersion",
|
||||
"POST /sysExportTemplate/createSysExportTemplate", "DELETE /sysExportTemplate/deleteSysExportTemplate", "DELETE /sysExportTemplate/deleteSysExportTemplateByIds", "PUT /sysExportTemplate/updateSysExportTemplate", "POST /sysExportTemplate/importExcel",
|
||||
"DELETE /sysError/deleteSysError", "DELETE /sysError/deleteSysErrorByIds", "PUT /sysError/updateSysError",
|
||||
"DELETE /sysLoginLog/deleteLoginLog", "DELETE /sysLoginLog/deleteLoginLogByIds", "DELETE /dataAccessLog/deleteDataAccessLogByIds",
|
||||
"POST /timedTask/createTimedTask", "PUT /timedTask/updateTimedTask", "DELETE /timedTask/deleteTimedTask", "POST /timedTask/toggleTimedTask", "POST /timedTask/triggerTimedTask",
|
||||
"POST /info/createInfo", "DELETE /info/deleteInfo", "DELETE /info/deleteInfoByIds", "PUT /info/updateInfo", "POST /email/emailTest", "POST /email/sendEmail",
|
||||
}
|
||||
out := make(map[string]struct{}, len(values))
|
||||
for _, value := range values {
|
||||
out[value] = struct{}{}
|
||||
}
|
||||
return out
|
||||
}()
|
||||
@@ -1,83 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
|
||||
const claimsKey = "admin_claims"
|
||||
|
||||
var refreshTokens singleflight.Group
|
||||
|
||||
func Auth(auth *service.AuthService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
token := c.GetHeader("x-token")
|
||||
if token == "" {
|
||||
token, _ = c.Cookie("x-token")
|
||||
}
|
||||
if token == "" {
|
||||
httpx.NoAuth(c, "未登录或非法访问,请登录")
|
||||
return
|
||||
}
|
||||
value, err, _ := refreshTokens.Do(token, func() (any, error) {
|
||||
return auth.AuthenticateToken(c.Request.Context(), token)
|
||||
})
|
||||
if err != nil {
|
||||
message := "无法处理此token"
|
||||
switch {
|
||||
case errors.Is(err, biz.ErrTokenExpired):
|
||||
message = "登录已过期,请重新登录"
|
||||
case errors.Is(err, biz.ErrTokenMalformed):
|
||||
message = "这不是一个token"
|
||||
case errors.Is(err, biz.ErrTokenSignatureInvalid):
|
||||
message = "无效签名"
|
||||
case errors.Is(err, biz.ErrTokenNotValidYet):
|
||||
message = "token尚未激活"
|
||||
case errors.Is(err, biz.ErrTokenDisabled):
|
||||
message = "您的帐户异地登陆或令牌失效"
|
||||
}
|
||||
httpx.SetTokenCookie(c, "", -1)
|
||||
httpx.NoAuth(c, message)
|
||||
return
|
||||
}
|
||||
authentication := value.(*biz.TokenAuthentication)
|
||||
if authentication.Refreshed != nil {
|
||||
c.Header("new-token", authentication.Refreshed.Value)
|
||||
c.Header("new-expires-at", strconv.FormatInt(authentication.Refreshed.ExpiresAt.Unix(), 10))
|
||||
httpx.SetTokenCookie(c, authentication.Refreshed.Value, int(authentication.Refreshed.TTL.Seconds()))
|
||||
}
|
||||
c.Set(claimsKey, authentication.Claims)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func Claims(c *gin.Context) *biz.AuthClaims {
|
||||
value, _ := c.Get(claimsKey)
|
||||
claims, _ := value.(*biz.AuthClaims)
|
||||
return claims
|
||||
}
|
||||
|
||||
func MustChangePassword() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
claims := Claims(c)
|
||||
if claims == nil || !claims.MustChangePwd {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
path := strings.TrimSuffix(c.Request.URL.Path, "/")
|
||||
if strings.HasSuffix(path, "/user/changePassword") || strings.HasSuffix(path, "/user/getUserInfo") || strings.HasSuffix(path, "/jwt/jsonInBlacklist") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
c.AbortWithStatusJSON(http.StatusConflict, httpx.Response{Code: httpx.CodePasswordChangeRequired, Data: gin.H{"needChangePassword": true}, Msg: "密码已过期,请先修改密码"})
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// The global access logger is the single request/response capture point.
|
||||
// OperationAudit consumes these values after the handler returns, avoiding a
|
||||
// second body read/writer wrapper.
|
||||
const (
|
||||
ctxReqBodyKey = "kra_req_body"
|
||||
ctxRespBufferKey = "kra_resp_buffer"
|
||||
)
|
||||
|
||||
type captureWriter struct {
|
||||
gin.ResponseWriter
|
||||
body bytes.Buffer
|
||||
maxBytes int
|
||||
}
|
||||
|
||||
func (w *captureWriter) Write(data []byte) (int, error) {
|
||||
limit := w.maxBytes
|
||||
if limit <= 0 {
|
||||
limit = 1024
|
||||
}
|
||||
if w.body.Len() < limit {
|
||||
remaining := limit - w.body.Len()
|
||||
if len(data) > remaining {
|
||||
_, _ = w.body.Write(data[:remaining])
|
||||
} else {
|
||||
_, _ = w.body.Write(data)
|
||||
}
|
||||
}
|
||||
return w.ResponseWriter.Write(data)
|
||||
}
|
||||
|
||||
func redactJSON(raw []byte, contentType string, limit int) string {
|
||||
if len(raw) == 0 {
|
||||
return ""
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 1024
|
||||
}
|
||||
text := string(raw)
|
||||
if !strings.Contains(strings.ToLower(contentType), "json") {
|
||||
if len(text) > limit {
|
||||
return "[超出记录长度]"
|
||||
}
|
||||
return text
|
||||
}
|
||||
var value any
|
||||
if json.Unmarshal(raw, &value) != nil {
|
||||
if len(text) > limit {
|
||||
return "[超出记录长度]"
|
||||
}
|
||||
return text
|
||||
}
|
||||
maskOperationBody(value)
|
||||
encoded, _ := json.Marshal(value)
|
||||
if len(encoded) > limit {
|
||||
return "[超出记录长度]"
|
||||
}
|
||||
return string(encoded)
|
||||
}
|
||||
|
||||
func stringValue(value any) string { text, _ := value.(string); return text }
|
||||
|
||||
func isBootstrapPath(path string) bool {
|
||||
return strings.HasSuffix(path, "/health") || strings.Contains(path, "/init/")
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"kra/internal/conf"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultCORSHeaders = "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id"
|
||||
defaultCORSMethods = "POST, GET, OPTIONS,DELETE,PUT"
|
||||
defaultCORSExpose = "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type, New-Token, New-Expires-At"
|
||||
)
|
||||
|
||||
// CORS applies the current administration CORS rules on every request so a
|
||||
// configuration reload takes effect without rebuilding the Gin engine.
|
||||
func CORS(runtime *conf.Runtime) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
config := runtime.Admin()
|
||||
if config == nil || config.Cors == nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
mode := strings.TrimSpace(config.Cors.Mode)
|
||||
origin := c.GetHeader("Origin")
|
||||
corsHandled := false
|
||||
if mode == "allow-all" {
|
||||
setCORSHeaders(c, origin, defaultCORSHeaders, defaultCORSMethods, defaultCORSExpose, true)
|
||||
corsHandled = true
|
||||
} else if rule := matchingCORSRule(config.Cors.Whitelist, origin); rule != nil {
|
||||
setCORSHeaders(c, rule.AllowOrigin, rule.AllowHeaders, rule.AllowMethods, rule.ExposeHeaders, rule.AllowCredentials)
|
||||
corsHandled = true
|
||||
} else if mode == "strict-whitelist" && !(c.Request.Method == http.MethodGet && c.Request.URL.Path == "/health") {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if corsHandled && c.Request.Method == http.MethodOptions {
|
||||
c.AbortWithStatus(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func matchingCORSRule(rules []*conf.AdminBackend_CORSRule, origin string) *conf.AdminBackend_CORSRule {
|
||||
for _, rule := range rules {
|
||||
if rule != nil && origin == rule.AllowOrigin {
|
||||
return rule
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setCORSHeaders(c *gin.Context, origin, headers, methods, expose string, credentials bool) {
|
||||
c.Header("Access-Control-Allow-Origin", origin)
|
||||
c.Header("Access-Control-Allow-Headers", headers)
|
||||
c.Header("Access-Control-Allow-Methods", methods)
|
||||
c.Header("Access-Control-Expose-Headers", expose)
|
||||
if credentials {
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"kra/internal/conf"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func runCORSTest(t *testing.T, admin *conf.AdminBackend, method, origin string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
engine := gin.New()
|
||||
engine.Use(CORS(conf.NewRuntime(nil, admin)))
|
||||
engine.Any("/test", func(c *gin.Context) { c.Status(http.StatusOK) })
|
||||
request := httptest.NewRequest(method, "/test", nil)
|
||||
if origin != "" {
|
||||
request.Header.Set("Origin", origin)
|
||||
}
|
||||
response := httptest.NewRecorder()
|
||||
engine.ServeHTTP(response, request)
|
||||
return response
|
||||
}
|
||||
|
||||
func TestCORSDoesNotConsumeUnmatchedWhitelistPreflight(t *testing.T) {
|
||||
response := runCORSTest(t, &conf.AdminBackend{Cors: &conf.AdminBackend_CORS{Mode: "whitelist"}}, http.MethodOptions, "https://unknown.example")
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("unmatched whitelist preflight status = %d, want %d", response.Code, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORSConsumesMatchedWhitelistPreflight(t *testing.T) {
|
||||
response := runCORSTest(t, &conf.AdminBackend{Cors: &conf.AdminBackend_CORS{Mode: "whitelist", Whitelist: []*conf.AdminBackend_CORSRule{{AllowOrigin: "https://admin.example"}}}}, http.MethodOptions, "https://admin.example")
|
||||
if response.Code != http.StatusNoContent {
|
||||
t.Fatalf("matched whitelist preflight status = %d, want %d", response.Code, http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"kra/internal/server/httpx"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ErrorAudit emits a structured Error-level log for unexpected HTTP failures.
|
||||
// The logging core is the single persistence path for sys_error, matching the
|
||||
// reference behavior and avoiding duplicate rows for HTTP failures.
|
||||
func ErrorAudit(logger *slog.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
privateErrors := strings.TrimSpace(c.Errors.ByType(gin.ErrorTypePrivate).String())
|
||||
auditPersistFailed, _ := c.Get(ctxOperationAuditPersistFailedKey)
|
||||
// sysError writes must never audit themselves. Log-viewer failures are
|
||||
// already recorded by the handler with the underlying filesystem error;
|
||||
// emitting again from the response envelope would duplicate both the
|
||||
// classified error file and the sys_error row.
|
||||
if auditPersistFailed != true && (strings.Contains(c.Request.URL.Path, "/sysError/") || strings.Contains(c.Request.URL.Path, "/logViewer/") || c.Writer.Status() >= 300 && c.Writer.Status() < 500) {
|
||||
return
|
||||
}
|
||||
var response httpx.Response
|
||||
var body []byte
|
||||
if value, ok := c.Get(ctxRespBufferKey); ok {
|
||||
if buffer, valid := value.(*bytes.Buffer); valid {
|
||||
body = buffer.Bytes()
|
||||
}
|
||||
}
|
||||
if json.Unmarshal(body, &response) != nil && privateErrors == "" {
|
||||
return
|
||||
}
|
||||
if response.Code == httpx.CodeSuccess && privateErrors == "" {
|
||||
return
|
||||
}
|
||||
if privateErrors == "" && expectedClientFailure(response.Msg) {
|
||||
return
|
||||
}
|
||||
errorMessage := response.Msg
|
||||
if privateErrors != "" {
|
||||
errorMessage = privateErrors
|
||||
}
|
||||
requestID, _ := c.Get("request_id")
|
||||
if logger != nil {
|
||||
logger.ErrorContext(c.Request.Context(), "请求处理失败", "mod", failureLogModule(c.Request.URL.Path), "path", c.Request.URL.Path, "method", c.Request.Method, "status", c.Writer.Status(), "error", errorMessage, "request_id", stringValue(requestID), "trace_id", stringValueFromContext(c, "trace_id"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func failureLogModule(path string) string {
|
||||
for _, marker := range []string{"/fileUploadAndDownload/", "/mediaUpload/", "/attachmentCategory/"} {
|
||||
if strings.Contains(path, marker) {
|
||||
return "upload"
|
||||
}
|
||||
}
|
||||
if strings.Contains(path, "/timedTask/") {
|
||||
return "timedTask"
|
||||
}
|
||||
if strings.Contains(path, "/logViewer/") {
|
||||
return "log-viewer"
|
||||
}
|
||||
return "biz"
|
||||
}
|
||||
|
||||
func expectedClientFailure(message string) bool {
|
||||
for _, value := range []string{"参数错误", "请输入用户名和密码", "验证码错误", "用户名不存在或者密码错误", "用户被禁止登录", "账号已锁定", "权限不足", "密码已过期", "未登录", "token", "令牌失效"} {
|
||||
if strings.Contains(message, value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func runErrorAudit(t *testing.T, path, response string, logger *slog.Logger) {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
engine.Use(ErrorAudit(logger))
|
||||
engine.GET(path, func(c *gin.Context) {
|
||||
c.Set(ctxRespBufferKey, bytes.NewBufferString(response))
|
||||
c.Set("request_id", "request-1")
|
||||
c.Set("trace_id", "trace-1")
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
request := httptest.NewRequest(http.MethodGet, path, nil)
|
||||
responseRecorder := httptest.NewRecorder()
|
||||
engine.ServeHTTP(responseRecorder, request)
|
||||
}
|
||||
|
||||
func TestErrorAuditEmitsUnexpectedBusinessFailure(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&output, nil))
|
||||
|
||||
runErrorAudit(t, "/test", `{"code":7,"data":{},"msg":"数据库写入失败"}`, logger)
|
||||
|
||||
if text := output.String(); !strings.Contains(text, `"mod":"biz"`) || !strings.Contains(text, `"error":"数据库写入失败"`) {
|
||||
t.Fatalf("unexpected error log: %s", text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorAuditUsesFeatureModule(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&output, nil))
|
||||
|
||||
runErrorAudit(t, "/timedTask/triggerTimedTask", `{"code":7,"data":{},"msg":"任务执行失败"}`, logger)
|
||||
if text := output.String(); !strings.Contains(text, `"mod":"timedTask"`) {
|
||||
t.Fatalf("unexpected feature log module: %s", text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorAuditSkipsExpectedClientFailure(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&output, nil))
|
||||
|
||||
runErrorAudit(t, "/test", `{"code":7,"data":{},"msg":"参数错误"}`, logger)
|
||||
|
||||
if output.Len() != 0 {
|
||||
t.Fatalf("expected no error log, got %s", output.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorAuditAllowsNilLogger(t *testing.T) {
|
||||
runErrorAudit(t, "/test", `{"code":7,"data":{},"msg":"数据库写入失败"}`, nil)
|
||||
}
|
||||
|
||||
func TestErrorAuditSkipsLogViewerFailureAlreadyLoggedByHandler(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&output, nil))
|
||||
|
||||
runErrorAudit(t, "/logViewer/content", `{"code":7,"data":{},"msg":"日志文件不可读取"}`, logger)
|
||||
|
||||
if output.Len() != 0 {
|
||||
t.Fatalf("log viewer failure must not be emitted twice, got %s", output.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorAuditEmitsPrivateMiddlewareErrorOnSuccessResponse(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&output, nil))
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
engine.Use(ErrorAudit(logger))
|
||||
engine.GET("/test", func(c *gin.Context) {
|
||||
c.Error(errors.New("operation audit persist: database unavailable"))
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{}, "msg": "成功"})
|
||||
})
|
||||
response := httptest.NewRecorder()
|
||||
engine.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/test", nil))
|
||||
if !strings.Contains(output.String(), "operation audit persist: database unavailable") {
|
||||
t.Fatalf("private middleware error was not logged: %s", output.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorAuditEmitsOperationAuditFailureForSysErrorRoute(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&output, nil))
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
engine.Use(ErrorAudit(logger))
|
||||
engine.PUT("/sysError/updateSysError", func(c *gin.Context) {
|
||||
c.Set(ctxOperationAuditPersistFailedKey, true)
|
||||
c.Error(errors.New("operation audit persist: database unavailable"))
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{}, "msg": "成功"})
|
||||
})
|
||||
response := httptest.NewRecorder()
|
||||
engine.ServeHTTP(response, httptest.NewRequest(http.MethodPut, "/sysError/updateSysError", nil))
|
||||
if !strings.Contains(output.String(), "operation audit persist: database unavailable") {
|
||||
t.Fatalf("sysError operation audit failure was not logged: %s", output.String())
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SecurityRateLimit(settings *service.SecurityService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
path := strings.TrimSuffix(c.Request.URL.Path, "/")
|
||||
if !strings.HasSuffix(path, "/base/login") && !strings.HasSuffix(path, "/base/captcha") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
config, err := settings.CurrentSecurity(c.Request.Context())
|
||||
if err != nil || config == nil {
|
||||
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"code": httpx.CodeError, "msg": "安全服务暂不可用"})
|
||||
return
|
||||
}
|
||||
if !config.LimitEnable {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
window := config.LimitWindow
|
||||
if window < 1 {
|
||||
window = 60
|
||||
}
|
||||
key := "KRA_SecLimit" + c.ClientIP() + c.FullPath()
|
||||
count, cacheErr := settings.IncrementRateLimit(c.Request.Context(), key, time.Duration(window)*time.Second)
|
||||
if cacheErr != nil {
|
||||
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"code": httpx.CodeError, "msg": "安全服务暂不可用"})
|
||||
return
|
||||
}
|
||||
if int(count) > config.LimitCount {
|
||||
c.JSON(200, gin.H{"code": httpx.CodeError, "msg": "请求太过频繁,请稍后再试"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type rateLimitSecurityRepo struct{}
|
||||
|
||||
func (rateLimitSecurityRepo) SecurityConfig(context.Context) (*biz.SecurityConfig, error) {
|
||||
return &biz.SecurityConfig{LimitEnable: true, LimitWindow: 60, LimitCount: 1}, nil
|
||||
}
|
||||
func (rateLimitSecurityRepo) SaveSecurityConfig(context.Context, *biz.SecurityConfig) error {
|
||||
return nil
|
||||
}
|
||||
func (rateLimitSecurityRepo) BackfillPasswordUpdatedAt(context.Context, time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type rateLimitCache struct{ err error }
|
||||
|
||||
func (rateLimitCache) Get(context.Context, string) (string, bool, error) { return "", false, nil }
|
||||
func (rateLimitCache) Set(context.Context, string, string, time.Duration) error { return nil }
|
||||
func (rateLimitCache) Delete(context.Context, string) error { return nil }
|
||||
func (c rateLimitCache) Increment(context.Context, string, time.Duration) (int64, error) {
|
||||
return 2, c.err
|
||||
}
|
||||
|
||||
func TestSecurityRateLimitMatchesResponseContract(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
settings := service.NewSecurityService(biz.NewSecurityUsecase(rateLimitSecurityRepo{}, rateLimitCache{}, nil, nil))
|
||||
engine := gin.New()
|
||||
engine.Use(SecurityRateLimit(settings))
|
||||
engine.POST("/base/login", func(c *gin.Context) { c.Status(http.StatusNoContent) })
|
||||
|
||||
response := httptest.NewRecorder()
|
||||
engine.ServeHTTP(response, httptest.NewRequest(http.MethodPost, "/base/login", nil))
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body=%s", response.Code, response.Body.String())
|
||||
}
|
||||
body := response.Body.String()
|
||||
if !strings.Contains(body, `"code":7`) || !strings.Contains(body, `"msg":"请求太过频繁,请稍后再试"`) || strings.Contains(body, `"data"`) {
|
||||
t.Fatalf("unexpected rate-limit response: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecurityRateLimitFailsClosedWhenCacheIsUnavailable(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
settings := service.NewSecurityService(biz.NewSecurityUsecase(rateLimitSecurityRepo{}, rateLimitCache{err: errors.New("cache unavailable")}, nil, nil))
|
||||
engine := gin.New()
|
||||
called := false
|
||||
engine.Use(SecurityRateLimit(settings))
|
||||
engine.POST("/base/login", func(c *gin.Context) { called = true })
|
||||
|
||||
response := httptest.NewRecorder()
|
||||
engine.ServeHTTP(response, httptest.NewRequest(http.MethodPost, "/base/login", nil))
|
||||
if response.Code != http.StatusServiceUnavailable || called {
|
||||
t.Fatalf("status=%d called=%v body=%s", response.Code, called, response.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func Recovery(logger *slog.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
defer func() {
|
||||
panicValue := recover()
|
||||
if panicValue == nil {
|
||||
return
|
||||
}
|
||||
brokenPipe := false
|
||||
if networkError, ok := panicValue.(*net.OpError); ok {
|
||||
if syscallError, ok := networkError.Err.(*os.SyscallError); ok {
|
||||
message := strings.ToLower(syscallError.Error())
|
||||
brokenPipe = strings.Contains(message, "broken pipe") || strings.Contains(message, "connection reset by peer")
|
||||
}
|
||||
}
|
||||
request, _ := httputil.DumpRequest(c.Request, false)
|
||||
if logger != nil {
|
||||
logger.ErrorContext(c.Request.Context(), "recovery from panic", "mod", "http", "error", panicValue, "request", string(request), "stack", string(debug.Stack()))
|
||||
}
|
||||
if brokenPipe {
|
||||
if err, ok := panicValue.(error); ok {
|
||||
_ = c.Error(err)
|
||||
}
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
}()
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
|
||||
"kra/internal/logging"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func randomHex(bytes int) string {
|
||||
value := make([]byte, bytes)
|
||||
_, _ = rand.Read(value)
|
||||
return hex.EncodeToString(value)
|
||||
}
|
||||
|
||||
func RequestMeta() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
requestID := c.GetHeader("X-Request-Id")
|
||||
if !saneHeaderID(requestID) {
|
||||
requestID = uuid.NewString()
|
||||
}
|
||||
traceID, parentSpanID := "", ""
|
||||
if upstreamTraceID, upstreamSpanID, ok := parseTraceparent(c.GetHeader("traceparent")); ok {
|
||||
traceID, parentSpanID = upstreamTraceID, upstreamSpanID
|
||||
} else if candidate := c.GetHeader("X-Trace-Id"); saneHeaderID(candidate) {
|
||||
traceID = candidate
|
||||
}
|
||||
if traceID == "" {
|
||||
traceID = randomHex(16)
|
||||
}
|
||||
spanID := randomHex(8)
|
||||
c.Header("X-Request-Id", requestID)
|
||||
c.Header("X-Trace-Id", traceID)
|
||||
if validTraceID(traceID) {
|
||||
c.Header("traceparent", "00-"+traceID+"-"+spanID+"-01")
|
||||
}
|
||||
c.Set("request_id", requestID)
|
||||
c.Set("trace_id", traceID)
|
||||
c.Set("span_id", spanID)
|
||||
c.Set("parent_span_id", parentSpanID)
|
||||
c.Request = c.Request.WithContext(logging.WithContextFields(c.Request.Context(), &logging.ContextFields{
|
||||
RequestID: requestID, TraceID: traceID, SpanID: spanID, ParentSpanID: parentSpanID,
|
||||
DeviceID: c.GetHeader("X-Device-Id"), ClientIP: c.ClientIP(),
|
||||
HTTPMethod: c.Request.Method, HTTPPath: c.Request.URL.Path,
|
||||
}))
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func parseTraceparent(value string) (traceID, parentSpanID string, ok bool) {
|
||||
parts := strings.Split(value, "-")
|
||||
if len(parts) < 4 {
|
||||
return "", "", false
|
||||
}
|
||||
version, traceID, parentSpanID, flags := parts[0], parts[1], parts[2], parts[3]
|
||||
if !lowerHex(version, 2) || version == "ff" || version == "00" && len(parts) != 4 ||
|
||||
!validTraceID(traceID) || !lowerHex(parentSpanID, 16) || allZero(parentSpanID) || !lowerHex(flags, 2) {
|
||||
return "", "", false
|
||||
}
|
||||
return traceID, parentSpanID, true
|
||||
}
|
||||
|
||||
func saneHeaderID(value string) bool {
|
||||
if value == "" || len(value) > 64 {
|
||||
return false
|
||||
}
|
||||
for index := 0; index < len(value); index++ {
|
||||
if value[index] <= 0x20 || value[index] > 0x7e {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validTraceID(value string) bool { return lowerHex(value, 32) && !allZero(value) }
|
||||
|
||||
func lowerHex(value string, length int) bool {
|
||||
if len(value) != length {
|
||||
return false
|
||||
}
|
||||
for index := 0; index < len(value); index++ {
|
||||
if current := value[index]; current < '0' || current > '9' {
|
||||
if current < 'a' || current > 'f' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func allZero(value string) bool {
|
||||
for index := 0; index < len(value); index++ {
|
||||
if value[index] != '0' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"kra/internal/server/handler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterAnnouncement(private, public *gin.RouterGroup, h *handler.Announcement) {
|
||||
privateInfo := private.Group("/info")
|
||||
privateInfo.POST("/createInfo", h.Create)
|
||||
privateInfo.DELETE("/deleteInfo", h.Delete)
|
||||
privateInfo.DELETE("/deleteInfoByIds", h.DeleteByIDs)
|
||||
privateInfo.PUT("/updateInfo", h.Update)
|
||||
privateInfo.GET("/findInfo", h.Find)
|
||||
privateInfo.GET("/getInfoList", h.List)
|
||||
privateInfo.GET("/getInfoDataSource", h.DataSource)
|
||||
publicInfo := public.Group("/info")
|
||||
publicInfo.GET("/getInfoPublic", h.Public)
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"kra/internal/server/handler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterAPI(group, public *gin.RouterGroup, engine *gin.Engine, h *handler.API) {
|
||||
api := group.Group("/api")
|
||||
api.POST("/getApiList", h.List)
|
||||
api.POST("/getAllApis", h.All)
|
||||
api.POST("/createApi", h.Create)
|
||||
api.POST("/updateApi", h.Update)
|
||||
api.POST("/deleteApi", h.Delete)
|
||||
api.DELETE("/deleteApisByIds", h.DeleteByIDs)
|
||||
api.POST("/getApiById", h.Find)
|
||||
api.GET("/getApiGroups", h.Groups)
|
||||
api.GET("/getApiRoles", h.Roles)
|
||||
api.POST("/setApiRoles", h.SetRoles)
|
||||
api.GET("/syncApi", h.Sync(engine))
|
||||
api.POST("/ignoreApi", h.Ignore)
|
||||
api.POST("/enterSyncApi", h.ApplySync)
|
||||
public.Group("/api").GET("/freshCasbin", h.FreshCasbin)
|
||||
casbin := group.Group("/casbin")
|
||||
casbin.POST("/updateCasbin", h.SetPolicyPaths)
|
||||
casbin.POST("/getPolicyPathByAuthorityId", h.PolicyPaths)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"kra/internal/server/handler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterAPIToken(group *gin.RouterGroup, handler *handler.APIToken) {
|
||||
router := group.Group("/sysApiToken")
|
||||
router.POST("/createApiToken", handler.Create)
|
||||
router.POST("/getApiTokenList", handler.List)
|
||||
router.POST("/deleteApiToken", handler.Delete)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"kra/internal/server/handler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterAudit(group, public *gin.RouterGroup, h *handler.Audit) {
|
||||
operations := group.Group("/sysOperationRecord")
|
||||
operations.GET("/getSysOperationRecordList", h.Operations)
|
||||
operations.GET("/findSysOperationRecord", h.Operation)
|
||||
operations.DELETE("/deleteSysOperationRecord", h.DeleteOperation)
|
||||
operations.DELETE("/deleteSysOperationRecordByIds", h.DeleteOperations)
|
||||
logins := group.Group("/sysLoginLog")
|
||||
logins.GET("/getLoginLogList", h.Logins)
|
||||
logins.GET("/findLoginLog", h.Login)
|
||||
logins.DELETE("/deleteLoginLog", h.DeleteLogin)
|
||||
logins.DELETE("/deleteLoginLogByIds", h.DeleteLogins)
|
||||
access := group.Group("/dataAccessLog")
|
||||
access.POST("/getDataAccessLogList", h.DataAccess)
|
||||
access.DELETE("/deleteDataAccessLogByIds", h.DeleteDataAccess)
|
||||
viewer := group.Group("/logViewer")
|
||||
viewer.GET("/dates", h.LogDates)
|
||||
viewer.GET("/files", h.LogFiles)
|
||||
viewer.GET("/content", h.LogContent)
|
||||
errors := group.Group("/sysError")
|
||||
errors.DELETE("/deleteSysError", h.DeleteError)
|
||||
errors.DELETE("/deleteSysErrorByIds", h.DeleteErrors)
|
||||
errors.PUT("/updateSysError", h.UpdateError)
|
||||
errors.GET("/findSysError", h.Error)
|
||||
errors.GET("/getSysErrorList", h.Errors)
|
||||
public.Group("/sysError").POST("/createSysError", h.CreateError)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"kra/internal/server/handler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterAuthority(group *gin.RouterGroup, h *handler.Authority) {
|
||||
router := group.Group("/authority")
|
||||
router.POST("/getAuthorityList", h.List)
|
||||
router.POST("/createAuthority", h.Create)
|
||||
router.POST("/copyAuthority", h.Copy)
|
||||
router.PUT("/updateAuthority", h.Update)
|
||||
router.POST("/deleteAuthority", h.Delete)
|
||||
router.POST("/setRoleUsers", h.SetUsers)
|
||||
router.GET("/getUsersByAuthority", h.Users)
|
||||
router.POST("/setDataScope", h.SetDataScope)
|
||||
router.GET("/getDataScopeDepts", h.DataScopeDepartments)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"kra/internal/server/handler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterDictionary(group *gin.RouterGroup, h *handler.Dictionary) {
|
||||
dictionaries := group.Group("/sysDictionary")
|
||||
dictionaries.POST("/createSysDictionary", h.Create)
|
||||
dictionaries.PUT("/updateSysDictionary", h.Update)
|
||||
dictionaries.DELETE("/deleteSysDictionary", h.Delete)
|
||||
dictionaries.GET("/findSysDictionary", h.Find)
|
||||
dictionaries.GET("/getSysDictionaryList", h.List(false))
|
||||
dictionaries.GET("/getSysDictionaryListWithDetails", h.List(true))
|
||||
dictionaries.GET("/exportSysDictionary", h.Export)
|
||||
dictionaries.POST("/importSysDictionary", h.Import)
|
||||
details := group.Group("/sysDictionaryDetail")
|
||||
details.POST("/createSysDictionaryDetail", h.CreateDetail)
|
||||
details.PUT("/updateSysDictionaryDetail", h.UpdateDetail)
|
||||
details.DELETE("/deleteSysDictionaryDetail", h.DeleteDetail)
|
||||
details.GET("/findSysDictionaryDetail", h.FindDetail)
|
||||
details.GET("/getSysDictionaryDetailList", h.Details)
|
||||
details.GET("/getDictionaryTreeList", h.Tree(false))
|
||||
details.GET("/getDictionaryTreeListByType", h.Tree(true))
|
||||
details.GET("/getDictionaryDetailsByParent", h.DetailsByParent)
|
||||
details.GET("/getDictionaryPath", h.Path)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"kra/internal/server/handler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterEmail(group *gin.RouterGroup, h *handler.Email) {
|
||||
email := group.Group("/email")
|
||||
email.POST("/emailTest", h.Test)
|
||||
email.POST("/sendEmail", h.Send)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"kra/internal/server/handler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterExport(group, public *gin.RouterGroup, h *handler.Export) {
|
||||
router := group.Group("/sysExportTemplate")
|
||||
router.POST("/createSysExportTemplate", h.Create)
|
||||
router.PUT("/updateSysExportTemplate", h.Update)
|
||||
router.DELETE("/deleteSysExportTemplate", h.Delete)
|
||||
router.DELETE("/deleteSysExportTemplateByIds", h.DeleteMany)
|
||||
router.GET("/findSysExportTemplate", h.Find)
|
||||
router.GET("/getSysExportTemplateList", h.List)
|
||||
router.GET("/previewSQL", h.Preview)
|
||||
router.GET("/exportExcel", h.Issue(false))
|
||||
router.GET("/exportTemplate", h.Issue(true))
|
||||
router.POST("/importExcel", h.Import)
|
||||
pub := public.Group("/sysExportTemplate")
|
||||
pub.GET("/exportExcelByToken", h.Download(false))
|
||||
pub.GET("/exportTemplateByToken", h.Download(true))
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"kra/internal/server/handler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterMedia(group *gin.RouterGroup, h *handler.Media) {
|
||||
files := group.Group("/fileUploadAndDownload")
|
||||
files.POST("/upload", h.Upload)
|
||||
files.POST("/getFileList", h.List)
|
||||
files.POST("/deleteFile", h.Delete)
|
||||
files.POST("/deleteFiles", h.DeleteMany)
|
||||
files.GET("/findFile", h.Find)
|
||||
files.POST("/editFileName", h.Rename)
|
||||
files.POST("/importURL", h.ImportURLs)
|
||||
files.POST("/listOssFiles", h.Storage)
|
||||
categories := group.Group("/attachmentCategory")
|
||||
categories.GET("/getCategoryList", h.Categories)
|
||||
categories.POST("/addCategory", h.SaveCategory)
|
||||
categories.POST("/deleteCategory", h.DeleteCategory)
|
||||
upload := group.Group("/mediaUpload")
|
||||
upload.POST("/init", h.InitUpload)
|
||||
upload.POST("/chunk", h.SaveChunk)
|
||||
upload.POST("/complete", h.CompleteUpload)
|
||||
upload.DELETE("/:uploadId", h.CancelUpload)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"kra/internal/server/handler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterMenu(group *gin.RouterGroup, h *handler.Menu) {
|
||||
router := group.Group("/menu")
|
||||
router.POST("/getMenuList", h.List)
|
||||
router.POST("/getBaseMenuTree", h.Tree)
|
||||
router.POST("/addBaseMenu", h.Create)
|
||||
router.POST("/updateBaseMenu", h.Update)
|
||||
router.POST("/deleteBaseMenu", h.Delete)
|
||||
router.POST("/getBaseMenuById", h.Find)
|
||||
router.POST("/addMenuAuthority", h.SetAuthorityMenus)
|
||||
router.POST("/getMenuAuthority", h.AuthorityMenus)
|
||||
router.GET("/getMenuRoles", h.RoleIDs)
|
||||
router.POST("/setMenuRoles", h.SetRoles)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"kra/internal/server/handler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterOrganization(group *gin.RouterGroup, h *handler.Organization) {
|
||||
departments := group.Group("/department")
|
||||
departments.POST("/getDepartmentList", h.ListDepartments)
|
||||
departments.POST("/createDepartment", h.CreateDepartment)
|
||||
departments.PUT("/updateDepartment", h.UpdateDepartment)
|
||||
departments.DELETE("/deleteDepartment", h.DeleteDepartment)
|
||||
departments.GET("/findDepartment", h.FindDepartment)
|
||||
departments.GET("/getDepartmentUsers", h.DepartmentUsers)
|
||||
departments.POST("/setDepartmentUsers", h.SetDepartmentUsers)
|
||||
group.POST("/user/setUserDepartments", h.SetUserDepartments)
|
||||
|
||||
positions := group.Group("/position")
|
||||
positions.POST("/getPositionList", h.ListPositions)
|
||||
positions.POST("/createPosition", h.CreatePosition)
|
||||
positions.PUT("/updatePosition", h.UpdatePosition)
|
||||
positions.DELETE("/deletePosition", h.DeletePosition)
|
||||
positions.GET("/findPosition", h.FindPosition)
|
||||
positions.GET("/getPositionUsers", h.PositionUsers)
|
||||
positions.POST("/setPositionUsers", h.SetPositionUsers)
|
||||
group.POST("/user/setUserPositions", h.SetUserPositions)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"kra/internal/server/handler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterParameter(group *gin.RouterGroup, handler *handler.Parameter) {
|
||||
router := group.Group("/sysParams")
|
||||
router.POST("/createSysParams", handler.Create)
|
||||
router.PUT("/updateSysParams", handler.Update)
|
||||
router.DELETE("/deleteSysParams", handler.Delete)
|
||||
router.DELETE("/deleteSysParamsByIds", handler.DeleteMany)
|
||||
router.GET("/findSysParams", handler.Find)
|
||||
router.GET("/getSysParam", handler.Get)
|
||||
router.GET("/getSysParamsList", handler.List)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"kra/internal/server/handler"
|
||||
)
|
||||
|
||||
func RegisterPayment(group, public *gin.RouterGroup, h *handler.Payment) {
|
||||
payment := group.Group("/payment")
|
||||
payment.GET("/configs", h.Configs)
|
||||
payment.POST("/config", h.SaveConfig)
|
||||
payment.GET("/orders", h.Orders)
|
||||
payment.POST("/order", h.Order)
|
||||
payment.POST("/create", h.Create)
|
||||
payment.POST("/query", h.Query)
|
||||
payment.POST("/refund", h.Refund)
|
||||
public.POST("/payment/callback/:provider", h.Callback)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"kra/internal/server/handler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterPermission(group *gin.RouterGroup, h *handler.Permission) {
|
||||
buttons := group.Group("/authorityBtn")
|
||||
buttons.POST("/getAuthorityBtn", h.Buttons)
|
||||
buttons.POST("/setAuthorityBtn", h.SetButtons)
|
||||
buttons.POST("/canRemoveAuthorityBtn", h.CanRemoveButton)
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"kra/internal/server/handler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterPublic(group *gin.RouterGroup, engine *gin.Engine, handler *handler.Public) {
|
||||
base := group.Group("/base")
|
||||
base.POST("/captcha", handler.Captcha)
|
||||
base.POST("/login", handler.Login)
|
||||
init := group.Group("/init")
|
||||
init.POST("/checkdb", handler.CheckDatabase)
|
||||
init.POST("/initdb", handler.InitializeDatabase(engine))
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"kra/internal/server/handler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterSystemConfig(group *gin.RouterGroup, handler *handler.SystemConfig) {
|
||||
security := group.Group("/securityConfig")
|
||||
security.GET("/getSecurityConfig", handler.GetSecurity)
|
||||
security.POST("/setSecurityConfig", handler.SetSecurity)
|
||||
|
||||
system := group.Group("/system")
|
||||
system.POST("/getSystemConfig", handler.Get)
|
||||
system.POST("/setSystemConfig", handler.Set)
|
||||
system.POST("/reloadSystem", handler.Reload)
|
||||
system.POST("/getServerInfo", handler.ServerInfo)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"kra/internal/server/handler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterTask(group *gin.RouterGroup, h *handler.Task) {
|
||||
router := group.Group("/timedTask")
|
||||
router.POST("/createTimedTask", h.Create)
|
||||
router.PUT("/updateTimedTask", h.Update)
|
||||
router.DELETE("/deleteTimedTask", h.Delete)
|
||||
router.POST("/toggleTimedTask", h.Toggle)
|
||||
router.POST("/triggerTimedTask", h.Trigger)
|
||||
router.GET("/getTimedTaskList", h.List)
|
||||
router.GET("/getTimedTaskLogList", h.Logs)
|
||||
router.GET("/getRegisteredMethods", h.Methods)
|
||||
router.GET("/alertStream", h.AlertStream)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"kra/internal/server/handler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterUser(group *gin.RouterGroup, handler *handler.User) {
|
||||
user := group.Group("/user")
|
||||
user.POST("/getUserList", handler.List)
|
||||
user.POST("/admin_register", handler.Create)
|
||||
user.PUT("/setUserInfo", handler.Update)
|
||||
user.PUT("/setSelfInfo", handler.UpdateSelf)
|
||||
user.DELETE("/deleteUser", handler.Delete)
|
||||
user.POST("/resetPassword", handler.ResetPassword)
|
||||
user.POST("/changePassword", handler.ChangePassword)
|
||||
user.PUT("/setSelfSetting", handler.SetSelfSetting)
|
||||
user.POST("/setUserAuthorities", handler.SetAuthorities)
|
||||
user.POST("/setUserAuthority", handler.SwitchAuthority)
|
||||
user.GET("/getUserInfo", handler.Get)
|
||||
}
|
||||
|
||||
func RegisterNavigation(group *gin.RouterGroup, handler *handler.Navigation) {
|
||||
group.Group("/menu").POST("/getMenu", handler.Menu)
|
||||
}
|
||||
|
||||
func RegisterSession(group *gin.RouterGroup, handler *handler.Session) {
|
||||
group.Group("/jwt").POST("/jsonInBlacklist", handler.Logout)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"kra/internal/server/handler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterVersion(group *gin.RouterGroup, h *handler.Version) {
|
||||
router := group.Group("/sysVersion")
|
||||
router.DELETE("/deleteSysVersion", h.Delete)
|
||||
router.DELETE("/deleteSysVersionByIds", h.DeleteMany)
|
||||
router.GET("/findSysVersion", h.Find)
|
||||
router.GET("/getSysVersionList", h.List)
|
||||
router.POST("/exportVersion", h.Export)
|
||||
router.GET("/downloadVersionJson", h.Download)
|
||||
router.POST("/importVersion", h.Import)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"kra/internal/server/handler"
|
||||
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
// ProviderSet is server providers.
|
||||
var ProviderSet = wire.NewSet(NewGinEngine, NewGinServer, handler.NewAuthority, handler.NewMenu, handler.NewAPI, handler.NewPermission, handler.NewOrganization, handler.NewAnnouncement, handler.NewEmail, handler.NewPayment, handler.NewTask, handler.NewMedia, handler.NewAudit, handler.NewExport, handler.NewVersion, handler.NewDictionary, handler.NewParameter, handler.NewAPIToken, handler.NewSystemConfig, handler.NewPublic, handler.NewUser, handler.NewNavigation, handler.NewSession, handler.NewSet)
|
||||
@@ -1,170 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"kra/internal/routeinfo"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
"github.com/swaggo/swag"
|
||||
)
|
||||
|
||||
const swaggerInstanceName = "kra-admin"
|
||||
|
||||
var (
|
||||
swaggerPathParameter = regexp.MustCompile(`:([A-Za-z0-9_]+)`)
|
||||
swaggerRegistration sync.Once
|
||||
swaggerDocument runtimeSwaggerDocument
|
||||
)
|
||||
|
||||
type runtimeSwaggerDocument struct {
|
||||
mu sync.RWMutex
|
||||
doc string
|
||||
}
|
||||
|
||||
func (d *runtimeSwaggerDocument) ReadDoc() string {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
return d.doc
|
||||
}
|
||||
|
||||
func (d *runtimeSwaggerDocument) replace(doc string) {
|
||||
d.mu.Lock()
|
||||
d.doc = doc
|
||||
d.mu.Unlock()
|
||||
}
|
||||
|
||||
func registerSwagger(engine *gin.Engine, prefix, version string, logger *slog.Logger) {
|
||||
swaggerRegistration.Do(func() { swag.Register(swaggerInstanceName, &swaggerDocument) })
|
||||
swaggerDocument.replace(buildSwaggerDocument(engine.Routes(), prefix, version))
|
||||
path := strings.TrimSuffix(prefix, "/") + "/swagger/*any"
|
||||
engine.GET(path, ginSwagger.WrapHandler(
|
||||
swaggerFiles.Handler,
|
||||
ginSwagger.InstanceName(swaggerInstanceName),
|
||||
ginSwagger.URL("doc.json"),
|
||||
ginSwagger.PersistAuthorization(true),
|
||||
))
|
||||
if logger != nil {
|
||||
logger.Info("register swagger handler", "mod", "system", "path", path)
|
||||
}
|
||||
}
|
||||
|
||||
func buildSwaggerDocument(routes []gin.RouteInfo, prefix, version string) string {
|
||||
basePath := strings.TrimSuffix(prefix, "/")
|
||||
if basePath == "" {
|
||||
basePath = "/"
|
||||
}
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
if routes[i].Path == routes[j].Path {
|
||||
return routes[i].Method < routes[j].Method
|
||||
}
|
||||
return routes[i].Path < routes[j].Path
|
||||
})
|
||||
paths := make(map[string]map[string]any, len(routes))
|
||||
for _, route := range routes {
|
||||
if strings.HasSuffix(route.Path, "/*filepath") {
|
||||
continue
|
||||
}
|
||||
method := strings.ToLower(route.Method)
|
||||
switch method {
|
||||
case "get", "post", "put", "delete", "patch":
|
||||
default:
|
||||
continue
|
||||
}
|
||||
apiPath := route.Path
|
||||
if prefix != "" {
|
||||
apiPath = strings.TrimPrefix(apiPath, strings.TrimSuffix(prefix, "/"))
|
||||
}
|
||||
if apiPath == "" {
|
||||
apiPath = "/"
|
||||
}
|
||||
documentPath := swaggerPathParameter.ReplaceAllString(apiPath, `{$1}`)
|
||||
group, description := routeinfo.Metadata(route.Method, apiPath)
|
||||
if description == "" {
|
||||
description = route.Method + " " + apiPath
|
||||
}
|
||||
operation := map[string]any{
|
||||
"tags": []string{group},
|
||||
"summary": description,
|
||||
"operationId": swaggerOperationID(route.Method, apiPath),
|
||||
"produces": []string{"application/json"},
|
||||
"responses": map[string]any{
|
||||
"200": map[string]any{"description": "OK", "schema": map[string]any{"$ref": "#/definitions/Response"}},
|
||||
},
|
||||
}
|
||||
if parameters := swaggerPathParameters(apiPath); len(parameters) > 0 {
|
||||
operation["parameters"] = parameters
|
||||
}
|
||||
if !swaggerPublicPath(apiPath) {
|
||||
operation["security"] = []map[string][]string{{"ApiKeyAuth": {}}}
|
||||
}
|
||||
if paths[documentPath] == nil {
|
||||
paths[documentPath] = map[string]any{}
|
||||
}
|
||||
paths[documentPath][method] = operation
|
||||
}
|
||||
document := map[string]any{
|
||||
"swagger": "2.0",
|
||||
"info": map[string]any{"title": "Kra Administration API", "version": version},
|
||||
"basePath": basePath,
|
||||
"schemes": []string{"http", "https"},
|
||||
"consumes": []string{"application/json"},
|
||||
"produces": []string{"application/json"},
|
||||
"securityDefinitions": map[string]any{
|
||||
"ApiKeyAuth": map[string]any{"type": "apiKey", "name": "x-token", "in": "header"},
|
||||
},
|
||||
"paths": paths,
|
||||
"definitions": map[string]any{
|
||||
"Response": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"code": map[string]any{"type": "integer"},
|
||||
"data": map[string]any{"type": "object"},
|
||||
"msg": map[string]any{"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
raw, _ := json.Marshal(document)
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
func swaggerOperationID(method, path string) string {
|
||||
value := strings.ToLower(method) + "_" + strings.Trim(path, "/")
|
||||
value = swaggerPathParameter.ReplaceAllString(value, "$1")
|
||||
return strings.Map(func(r rune) rune {
|
||||
if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '_' {
|
||||
return r
|
||||
}
|
||||
return '_'
|
||||
}, value)
|
||||
}
|
||||
|
||||
func swaggerPathParameters(path string) []map[string]any {
|
||||
matches := swaggerPathParameter.FindAllStringSubmatch(path, -1)
|
||||
parameters := make([]map[string]any, 0, len(matches))
|
||||
for _, match := range matches {
|
||||
parameters = append(parameters, map[string]any{"name": match[1], "in": "path", "required": true, "type": "string"})
|
||||
}
|
||||
return parameters
|
||||
}
|
||||
|
||||
func swaggerPublicPath(path string) bool {
|
||||
for _, marker := range []string{
|
||||
"/health", "/base/login", "/base/captcha", "/init/checkdb", "/init/initdb",
|
||||
"/api/freshCasbin", "/sysExportTemplate/exportExcelByToken", "/sysExportTemplate/exportTemplateByToken",
|
||||
"/sysError/createSysError", "/info/getInfoDataSource", "/info/getInfoPublic",
|
||||
} {
|
||||
if path == marker {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user