mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-12 10:16:37 +08:00
fix(api): align upload permissions and mount robots and swagger routes
This commit is contained in:
+2538
-34
File diff suppressed because it is too large
Load Diff
+2538
-34
File diff suppressed because it is too large
Load Diff
+1567
-33
File diff suppressed because it is too large
Load Diff
@@ -170,6 +170,10 @@ func (p *Plugin) Apply(ctx *core.Context) error {
|
|||||||
adminRouter := ctx.Router().Group("/api/v1/admin", loginMW, adminMW)
|
adminRouter := ctx.Router().Group("/api/v1/admin", loginMW, adminMW)
|
||||||
handler.RegisterRoutes(adminRouter)
|
handler.RegisterRoutes(adminRouter)
|
||||||
|
|
||||||
|
// Register robots.txt public route
|
||||||
|
ctx.Router().GET("/robots.txt", handler.GetRobotsTXT)
|
||||||
|
ctx.Router().RegisterWhitelist("/robots.txt")
|
||||||
|
|
||||||
// 2. Register Background Tasks
|
// 2. Register Background Tasks
|
||||||
logSwitchHandler := &service.LogDBSwitchHandler{}
|
logSwitchHandler := &service.LogDBSwitchHandler{}
|
||||||
ctx.Task().Register(service.LogDBSwitchTask, func(c context.Context, payload []byte) error {
|
ctx.Task().Register(service.LogDBSwitchTask, func(c context.Context, payload []byte) error {
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ func TestAdminPluginUnit(t *testing.T) {
|
|||||||
// Verify routes
|
// Verify routes
|
||||||
routes := ctx.Router().Routes()
|
routes := ctx.Router().Routes()
|
||||||
assert.NotEmpty(t, routes)
|
assert.NotEmpty(t, routes)
|
||||||
|
var hasRobots bool
|
||||||
|
for _, r := range routes {
|
||||||
|
if r.Path == "/robots.txt" && r.Method == "GET" {
|
||||||
|
hasRobots = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, hasRobots, "admin plugin must register /robots.txt")
|
||||||
|
|
||||||
// Verify tasks
|
// Verify tasks
|
||||||
_, ok := ctx.Tasks().Get("admin:system_cleanup")
|
_, ok := ctx.Tasks().Get("admin:system_cleanup")
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ func (p *Plugin) Apply(ctx *core.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
loginMW := authSvc.RequireAuthMiddleware().(gin.HandlerFunc)
|
loginMW := authSvc.RequireAuthMiddleware().(gin.HandlerFunc)
|
||||||
|
adminMW := authSvc.RequireAdminMiddleware().(gin.HandlerFunc)
|
||||||
|
|
||||||
// 0a. Register migrations
|
// 0a. Register migrations
|
||||||
ctx.Migrations().Register("upload", uploadMigrations)
|
ctx.Migrations().Register("upload", uploadMigrations)
|
||||||
@@ -123,16 +124,14 @@ func (p *Plugin) Apply(ctx *core.Context) error {
|
|||||||
uploadGroup := ctx.Router().Group("/api/v1/upload", loginMW)
|
uploadGroup := ctx.Router().Group("/api/v1/upload", loginMW)
|
||||||
{
|
{
|
||||||
uploadGroup.POST("", handler.UploadFile)
|
uploadGroup.POST("", handler.UploadFile)
|
||||||
uploadGroup.GET("", handler.ListFiles)
|
uploadGroup.DELETE("/:id", handler.DeleteMyFile)
|
||||||
uploadGroup.DELETE("/:id", handler.DeleteFile)
|
|
||||||
uploadGroup.POST("/batch-download", handler.BatchDownloadFiles)
|
|
||||||
uploadGroup.GET("/my", handler.ListMyFiles)
|
uploadGroup.GET("/my", handler.ListMyFiles)
|
||||||
uploadGroup.PUT("/:id", handler.UpdateMyFile)
|
uploadGroup.PUT("/:id", handler.UpdateMyFile)
|
||||||
uploadGroup.GET("/download/:id", handler.DownloadFile)
|
uploadGroup.GET("/download/:id", handler.DownloadFile)
|
||||||
uploadGroup.POST("/download/batch", handler.BatchDownloadFiles)
|
uploadGroup.POST("/download/batch", handler.BatchDownloadFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
adminUploadGroup := ctx.Router().Group("/api/v1/admin/uploads", loginMW)
|
adminUploadGroup := ctx.Router().Group("/api/v1/admin/uploads", loginMW, adminMW)
|
||||||
{
|
{
|
||||||
adminUploadGroup.GET("", handler.ListFiles)
|
adminUploadGroup.GET("", handler.ListFiles)
|
||||||
adminUploadGroup.GET("/stats", handler.GetFileStats)
|
adminUploadGroup.GET("/stats", handler.GetFileStats)
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ func (stubAuthService) RequireAuthMiddleware() any {
|
|||||||
return gin.HandlerFunc(func(c *gin.Context) { c.Next() })
|
return gin.HandlerFunc(func(c *gin.Context) { c.Next() })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (stubAuthService) RequireAdminMiddleware() any {
|
||||||
|
return gin.HandlerFunc(func(c *gin.Context) { c.Next() })
|
||||||
|
}
|
||||||
|
|
||||||
func TestUserUploadRoutes(t *testing.T) {
|
func TestUserUploadRoutes(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
ctx := core.NewContext(context.Background())
|
ctx := core.NewContext(context.Background())
|
||||||
@@ -34,12 +38,18 @@ func TestUserUploadRoutes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
want := []string{
|
want := []string{
|
||||||
|
"POST /api/v1/upload",
|
||||||
|
"DELETE /api/v1/upload/:id",
|
||||||
"GET /api/v1/upload/my",
|
"GET /api/v1/upload/my",
|
||||||
"PUT /api/v1/upload/:id",
|
"PUT /api/v1/upload/:id",
|
||||||
"GET /api/v1/upload/download/:id",
|
"GET /api/v1/upload/download/:id",
|
||||||
"POST /api/v1/upload/download/batch",
|
"POST /api/v1/upload/download/batch",
|
||||||
"GET /api/v1/upload",
|
"GET /api/v1/admin/uploads",
|
||||||
"POST /api/v1/upload/batch-download",
|
"GET /api/v1/admin/uploads/stats",
|
||||||
|
"DELETE /api/v1/admin/uploads/:id",
|
||||||
|
"GET /api/v1/admin/uploads/download/:id",
|
||||||
|
"POST /api/v1/admin/uploads/download/batch",
|
||||||
|
"GET /api/v1/admin/uploads/types",
|
||||||
}
|
}
|
||||||
found := make(map[string]bool, len(want))
|
found := make(map[string]bool, len(want))
|
||||||
for _, rd := range ctx.Router().Routes() {
|
for _, rd := range ctx.Router().Routes() {
|
||||||
|
|||||||
@@ -4,9 +4,12 @@
|
|||||||
package driver_http
|
package driver_http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"Wavelet/core"
|
||||||
|
|
||||||
"Wavelet/core/extpoints"
|
"Wavelet/core/extpoints"
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBuildEngineDefaultRedirectsTrailingSlash(t *testing.T) {
|
func TestBuildEngineDefaultRedirectsTrailingSlash(t *testing.T) {
|
||||||
@@ -111,3 +114,29 @@ func bindAppConfig(t *testing.T, values map[string]any, env map[string]string) h
|
|||||||
}
|
}
|
||||||
|
|
||||||
func boolPtr(v bool) *bool { return &v }
|
func boolPtr(v bool) *bool { return &v }
|
||||||
|
|
||||||
|
func TestDriverHTTPSwaggerMount(t *testing.T) {
|
||||||
|
ctx := core.NewContext(t.Context())
|
||||||
|
ctx.Config().SetSource(core.NewMapSource(map[string]any{"app.env": "development"}))
|
||||||
|
if err := ctx.Config().Resolve(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
p := New(WithAddr("127.0.0.1:0"))
|
||||||
|
if err := p.Apply(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
startCtx, cancel := context.WithCancel(t.Context())
|
||||||
|
defer cancel()
|
||||||
|
if err := p.Start(startCtx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = p.Stop(t.Context()) }()
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet, "/swagger/index.html", nil)
|
||||||
|
req.RequestURI = "/swagger/index.html"
|
||||||
|
p.Engine().ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200 for /swagger/index.html, got %d (body: %s)", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const indexFile = "index.html"
|
|||||||
|
|
||||||
// serverOwnedPrefixes are backend-owned namespaces. A miss there must keep Gin's default
|
// serverOwnedPrefixes are backend-owned namespaces. A miss there must keep Gin's default
|
||||||
// 404 instead of silently returning the frontend shell, which would mask broken API links.
|
// 404 instead of silently returning the frontend shell, which would mask broken API links.
|
||||||
var serverOwnedPrefixes = []string{"/api/", "/f/"}
|
var serverOwnedPrefixes = []string{"/api/", "/f/", "/swagger/"}
|
||||||
|
|
||||||
// registerFrontend mounts assets as the NoRoute fallback so client-side routes resolve.
|
// registerFrontend mounts assets as the NoRoute fallback so client-side routes resolve.
|
||||||
// It is a no-op when assets is nil, i.e. the binary was built without the embed_frontend tag.
|
// It is a no-op when assets is nil, i.e. the binary was built without the embed_frontend tag.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ package driver_http
|
|||||||
import (
|
import (
|
||||||
"Wavelet/core"
|
"Wavelet/core"
|
||||||
"Wavelet/core/contracts"
|
"Wavelet/core/contracts"
|
||||||
|
_ "Wavelet/docs" // swagger documentation registration
|
||||||
"Wavelet/pkg/util"
|
"Wavelet/pkg/util"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -17,6 +18,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
swaggerFiles "github.com/swaggo/files"
|
||||||
|
ginSwagger "github.com/swaggo/gin-swagger"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -207,6 +210,19 @@ func (p *Plugin) Start(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mount Swagger in non-production environments
|
||||||
|
if p.coreCtx != nil {
|
||||||
|
var appCfg httpAppConfig
|
||||||
|
_ = p.coreCtx.Config().Bind("app", &appCfg)
|
||||||
|
if appCfg.Env != "production" && appCfg.Env != "prod" {
|
||||||
|
swaggerHandler := ginSwagger.WrapHandler(swaggerFiles.Handler)
|
||||||
|
p.engine.GET("/swagger/*any", swaggerHandler)
|
||||||
|
if appCfg.APIPrefix != "" {
|
||||||
|
p.engine.GET(appCfg.APIPrefix+"/swagger/*any", swaggerHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
registerFrontend(p.engine, frontendAssets())
|
registerFrontend(p.engine, frontendAssets())
|
||||||
|
|
||||||
p.server = &http.Server{
|
p.server = &http.Server{
|
||||||
|
|||||||
Reference in New Issue
Block a user