refactor(repo): consolidate openflare-server to root and move subprojects to internal/apps

- Merge all files inside openflare-server to the repository root directory.
- Relocate agent, relay, and flared subprojects from internal/ to internal/apps/.
- Combine docker-compose files and update build context paths to root.
- Update GitHub workflows and Dockerfiles to refer to new directories and package names.
- Rewrite Go package imports across all files.
- Resolve database renew test race condition and clean up docs.
This commit is contained in:
ryan
2026-06-19 14:23:29 +08:00
parent 19d476ed7f
commit 63cd906cfc
1064 changed files with 366 additions and 1397 deletions
+112
View File
@@ -0,0 +1,112 @@
// Copyright 2025 linux.do
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package router 提供 HTTP 路由注册与服务启动
package router
import (
"context"
"net/http"
"strconv"
"strings"
"time"
"github.com/Rain-kl/Wavelet/internal/common/response"
"github.com/Rain-kl/Wavelet/internal/config"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/pkg/logger"
otel_trace "github.com/Rain-kl/Wavelet/pkg/trace"
"github.com/gin-gonic/gin"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
func loggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 初始化 Trace
ctx, span := otel_trace.Start(c.Request.Context(), "LoggerMiddleware")
defer span.End()
// 开始计时
start := time.Now()
// 记录请求路径和 Query
path := c.Request.URL.Path
raw := c.Request.URL.RawQuery
if raw != "" {
path = path + "?" + raw
}
// 执行请求
c.Next()
// 停止计时
end := time.Now()
latency := end.Sub(start)
// 打印日志
// 排除健康检查接口
healthPath := config.Config.App.APIPrefix + "/health"
if c.Request.URL.Path != healthPath {
logger.InfoF(
ctx,
"[LoggerMiddleware] %s %s\nStartTime: %s\nEndTime: %s\nLatency: %d\nClientIP: %s\nResponse: %d %d",
c.Request.Method,
path,
start.Format(time.RFC3339),
end.Format(time.RFC3339),
latency.Milliseconds(),
c.ClientIP(),
c.Writer.Status(),
c.Writer.Size(),
)
}
// 设置 Span 状态
if c.Writer.Status() >= http.StatusBadRequest {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, strconv.Itoa(c.Writer.Status()))
}
}
}
func isOriginAllowed(ctx context.Context, origin string) bool {
sc, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeyServerAddress)
if err != nil || sc.Value == "" {
return false
}
allowedOrigins := strings.Split(sc.Value, ",")
for _, allowed := range allowedOrigins {
allowed = strings.TrimRight(strings.TrimSpace(allowed), "/")
if allowed != "" && strings.EqualFold(allowed, origin) {
return true
}
}
return false
}
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
if origin != "" && isOriginAllowed(c.Request.Context(), origin) {
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With, X-Access-Token, X-Cap-Token")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH")
}
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
// errorHandlerMiddleware 委托给 response.ErrorHandlerMiddleware,保持路由层单一入口。
func errorHandlerMiddleware() gin.HandlerFunc {
return response.ErrorHandlerMiddleware()
}
+128
View File
@@ -0,0 +1,128 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package router
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/internal/testhelper"
"github.com/gin-gonic/gin"
)
func TestCORSMiddleware(t *testing.T) {
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
defer cleanup()
gin.SetMode(gin.TestMode)
clearConfigCache := func() {
if err := repository.InvalidateAllSystemConfigCaches(context.Background()); err != nil {
t.Fatalf("InvalidateAllSystemConfigCaches() error = %v", err)
}
}
t.Run("missing server_address configuration returns no CORS headers", func(t *testing.T) {
clearConfigCache()
// Ensure it's empty in DB
if err := dbConn.Model(&model.SystemConfig{}).Where("key = ?", model.ConfigKeyServerAddress).Update("value", "").Error; err != nil {
t.Fatalf("failed to update config: %v", err)
}
clearConfigCache()
r := gin.New()
r.Use(corsMiddleware())
r.GET("/test", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
req.Header.Set("Origin", "http://attacker.com")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200 OK, got %d", w.Code)
}
if val := w.Header().Get("Access-Control-Allow-Origin"); val != "" {
t.Errorf("expected empty Access-Control-Allow-Origin header, got %q", val)
}
if val := w.Header().Get("Access-Control-Allow-Credentials"); val != "" {
t.Errorf("expected empty Access-Control-Allow-Credentials header, got %q", val)
}
})
t.Run("matching server_address allows origin and sets credential headers", func(t *testing.T) {
clearConfigCache()
if err := dbConn.Model(&model.SystemConfig{}).Where("key = ?", model.ConfigKeyServerAddress).Update("value", "https://trusted.com, http://localhost:3000/").Error; err != nil {
t.Fatalf("failed to update config: %v", err)
}
clearConfigCache()
r := gin.New()
r.Use(corsMiddleware())
r.GET("/test", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
// Test trusted origin 1
req1, _ := http.NewRequest(http.MethodGet, "/test", nil)
req1.Header.Set("Origin", "https://trusted.com")
w1 := httptest.NewRecorder()
r.ServeHTTP(w1, req1)
if w1.Code != http.StatusOK {
t.Fatalf("expected 200 OK, got %d", w1.Code)
}
if val := w1.Header().Get("Access-Control-Allow-Origin"); val != "https://trusted.com" {
t.Errorf("expected Access-Control-Allow-Origin 'https://trusted.com', got %q", val)
}
if val := w1.Header().Get("Access-Control-Allow-Credentials"); val != "true" {
t.Errorf("expected Access-Control-Allow-Credentials 'true', got %q", val)
}
// Test trusted origin 2 (trimmed trailing slash)
req2, _ := http.NewRequest(http.MethodGet, "/test", nil)
req2.Header.Set("Origin", "http://localhost:3000")
w2 := httptest.NewRecorder()
r.ServeHTTP(w2, req2)
if w2.Code != http.StatusOK {
t.Fatalf("expected 200 OK, got %d", w2.Code)
}
if val := w2.Header().Get("Access-Control-Allow-Origin"); val != "http://localhost:3000" {
t.Errorf("expected Access-Control-Allow-Origin 'http://localhost:3000', got %q", val)
}
})
t.Run("non-matching origin is denied CORS headers", func(t *testing.T) {
clearConfigCache()
if err := dbConn.Model(&model.SystemConfig{}).Where("key = ?", model.ConfigKeyServerAddress).Update("value", "https://trusted.com").Error; err != nil {
t.Fatalf("failed to update config: %v", err)
}
clearConfigCache()
r := gin.New()
r.Use(corsMiddleware())
r.GET("/test", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
req.Header.Set("Origin", "https://attacker.com")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200 OK, got %d", w.Code)
}
if val := w.Header().Get("Access-Control-Allow-Origin"); val != "" {
t.Errorf("expected empty Access-Control-Allow-Origin header, got %q", val)
}
})
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package root registers custom business routes and frontend serving.
package root
import (
"github.com/gin-gonic/gin"
)
// RegisterCustomRootRoutes registers custom business routes that belong to the root path.
func RegisterCustomRootRoutes(_ *gin.Engine) {
// Add custom root routes here
}
+32
View File
@@ -0,0 +1,32 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package root
import (
_ "github.com/Rain-kl/Wavelet/docs" // Swagger documentation generation setup
publicconfig "github.com/Rain-kl/Wavelet/internal/apps/config"
"github.com/Rain-kl/Wavelet/internal/apps/health"
"github.com/Rain-kl/Wavelet/internal/apps/upload"
"github.com/Rain-kl/Wavelet/internal/config"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
// RegisterDefaultRootRoutes registers default routes that belong to the root path.
func RegisterDefaultRootRoutes(r *gin.Engine) {
// 1. Serve files by ID
r.GET("/f/:id", upload.ServeFileByID)
// 2. Dynamic robots.txt serving
r.GET("/robots.txt", publicconfig.GetRobotsTXT)
// 3. Swagger routes (Non-production only)
if !config.Config.App.IsProduction() {
r.GET(config.Config.App.APIPrefix+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}
// 4. Health check
r.GET(config.Config.App.APIPrefix+"/health", health.Health)
}
+110
View File
@@ -0,0 +1,110 @@
//go:build embed_frontend
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package root
import (
"embed"
"io"
"io/fs"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
//go:embed all:dist
var frontendFS embed.FS
func serveFileDirect(c *gin.Context, subFS fs.FS, filePath string) bool {
file, err := subFS.Open(filePath)
if err != nil {
return false
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
return false
}
if stat.IsDir() {
return false
}
seeker, ok := file.(io.ReadSeeker)
if !ok {
return false
}
// 使用 http.ServeContent 直接输出文件内容,不进行路径规范化重定向
http.ServeContent(c.Writer, c.Request, filePath, stat.ModTime(), seeker)
return true
}
func init() {
RegisterFrontend = func(r *gin.Engine) {
subFS, err := fs.Sub(frontendFS, "dist")
if err != nil {
panic(err)
}
r.NoRoute(func(c *gin.Context) {
path := c.Request.URL.Path
// API 接口路由或文件服务路由 -> 直接返回,由 Gin 处理标准 404
if strings.HasPrefix(path, "/api/") || strings.HasPrefix(path, "/f/") {
return
}
// 只处理 GET 和 HEAD 请求
if c.Request.Method != http.MethodGet && c.Request.Method != http.MethodHead {
c.JSON(http.StatusMethodNotAllowed, gin.H{"error_msg": "Method not allowed"})
return
}
// 移除开头的斜杠以在嵌入文件系统中查找
cleanPath := strings.TrimPrefix(path, "/")
// 1. 根路径 -> 直接输出 index.html
if cleanPath == "" {
if serveFileDirect(c, subFS, "index.html") {
return
}
}
// 2. 精确匹配(如果对应的文件存在,直接输出)
if serveFileDirect(c, subFS, cleanPath) {
return
}
// 如果是个目录(例如请求了 "/login",同时 dist 目录下存在一个叫 "login" 的文件夹目录),
// 则查找是否有对应的 ".html" 文件(例如 "login.html")并进行输出。
if cleanPath != "" {
htmlPath := cleanPath + ".html"
if serveFileDirect(c, subFS, htmlPath) {
return
}
}
// 3. Next.js Clean URLs 兜底逻辑(例如访问 /settings/security -> 实际映射输出 settings/security.html)
if !strings.Contains(cleanPath, ".") {
htmlPath := cleanPath + ".html"
if serveFileDirect(c, subFS, htmlPath) {
return
}
indexPath := cleanPath + "/index.html"
if serveFileDirect(c, subFS, indexPath) {
return
}
}
// 4. 单页应用(SPA)前端路由兜底:返回 index.html
if serveFileDirect(c, subFS, "index.html") {
return
}
})
}
}
+26
View File
@@ -0,0 +1,26 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package root registers custom business routes and frontend serving.
package root
import (
"github.com/gin-gonic/gin"
)
// RegisterFrontend is a package-level variable overridden by frontend.go when built with embed_frontend.
var RegisterFrontend = func(_ *gin.Engine) {
// No-op by default
}
// RegisterRootRoutes registers custom business routes that belong to the root path.
func RegisterRootRoutes(r *gin.Engine) {
// 1. Default root routes (/f/:id, /robots.txt, and /swagger/*any)
RegisterDefaultRootRoutes(r)
// 2. Register custom serving
RegisterCustomRootRoutes(r)
// 3. Register frontend serving
RegisterFrontend(r)
}
+123
View File
@@ -0,0 +1,123 @@
// Copyright 2025 linux.do
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package router
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/risk_control"
router_root "github.com/Rain-kl/Wavelet/internal/router/root"
v1 "github.com/Rain-kl/Wavelet/internal/router/v1"
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
"github.com/Rain-kl/Wavelet/internal/config"
otel_trace "github.com/Rain-kl/Wavelet/pkg/trace"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/redis"
"github.com/gin-gonic/gin"
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
)
// Serve 启动 HTTP API 服务
func Serve() {
// 运行模式
if config.Config.App.IsProduction() {
gin.SetMode(gin.ReleaseMode)
}
// 初始化路由
r := gin.New()
// Legacy OpenFlare list endpoints register both /resource and /resource/; disable auto slash redirects.
r.RedirectTrailingSlash = false
r.Use(gin.Recovery())
r.Use(corsMiddleware())
cfg := config.Config.Redis
addrs := cfg.Addrs
sessionAddr := "localhost:6379"
if len(addrs) > 0 {
sessionAddr = addrs[0]
}
sessionStore, err := redis.NewStoreWithDB(
cfg.MinIdleConn,
"tcp",
sessionAddr,
cfg.Username,
cfg.Password,
strconv.Itoa(cfg.DB),
[]byte(config.Config.App.SessionSecret),
)
if err != nil {
log.Fatalf("[API] init session store failed: %v\n", err)
}
// 设置 Session Redis Key 前缀
if cfg.KeyPrefix != "" {
if err := redis.SetKeyPrefix(sessionStore, cfg.KeyPrefix+"session:"); err != nil {
log.Printf("[API] set session key prefix failed: %v\n", err)
}
}
sessionStore.Options(oauth.GetSessionOptions(config.Config.App.SessionAge))
r.Use(sessions.Sessions(config.Config.App.SessionCookieName, sessionStore))
// 补充中间件
r.Use(otelgin.Middleware(config.Config.App.AppName), errorHandlerMiddleware(), loggerMiddleware(), risk_control.RiskControlMiddleware())
registerRoutes(r)
srv := &http.Server{
Addr: config.Config.App.Addr,
Handler: r,
ReadHeaderTimeout: 10 * time.Second,
}
go func() {
log.Printf("[API] server starting on %s\n", config.Config.App.Addr)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("[API] server failed: %v\n", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Duration(config.Config.App.GracefulShutdownTimeout)*time.Second)
otel_trace.Shutdown(shutdownCtx)
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("[API] server forced to shutdown: %v\n", err)
cancel()
os.Exit(1)
}
cancel()
log.Println("[API] server exited")
}
func registerRoutes(r *gin.Engine) {
// Register custom root routes, Swagger, and frontend serving
router_root.RegisterRootRoutes(r)
apiGroup := r.Group(config.Config.App.APIPrefix)
{
apiV1Router := apiGroup.Group("/v1")
{
v1.RegisterV1Routes(apiV1Router, apiGroup)
}
}
}
+217
View File
@@ -0,0 +1,217 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package v1 contains router registrations for API V1
package v1
import (
"github.com/Rain-kl/Wavelet/internal/apps/admin"
admin_auth_source "github.com/Rain-kl/Wavelet/internal/apps/admin/auth_source"
admin_cache "github.com/Rain-kl/Wavelet/internal/apps/admin/cache"
admin_db_manage "github.com/Rain-kl/Wavelet/internal/apps/admin/db_manage"
admin_logs "github.com/Rain-kl/Wavelet/internal/apps/admin/logs"
admin_push "github.com/Rain-kl/Wavelet/internal/apps/admin/push"
admin_status "github.com/Rain-kl/Wavelet/internal/apps/admin/status"
"github.com/Rain-kl/Wavelet/internal/apps/admin/system_config"
admin_task "github.com/Rain-kl/Wavelet/internal/apps/admin/task"
admin_template "github.com/Rain-kl/Wavelet/internal/apps/admin/template"
admin_updater "github.com/Rain-kl/Wavelet/internal/apps/admin/updater"
admin_user "github.com/Rain-kl/Wavelet/internal/apps/admin/user"
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
"github.com/Rain-kl/Wavelet/internal/apps/upload"
"github.com/gin-gonic/gin"
)
// RegisterAdminRoutes registers all admin-related routes with sub-group categorizations.
func RegisterAdminRoutes(apiV1Router *gin.RouterGroup) {
adminRouter := apiV1Router.Group("/admin")
adminRouter.Use(oauth.LoginRequired(), admin.LoginAdminRequired())
{
// 1. Diagnostics & Infrastructure Management
registerAdminDiagnosticRoutes(adminRouter)
// 2. Identity & Access Management (IAM)
registerAdminIAMRoutes(adminRouter)
// 3. System Configuration & Templates Settings
registerAdminConfigRoutes(adminRouter)
// 4. Storage & Asset Management
registerAdminStorageRoutes(adminRouter)
// 5. Task Orchestration & Automation
registerAdminTaskRoutes(adminRouter)
// 6. Messaging & Push Notifications
registerAdminPushRoutes(adminRouter)
}
}
// registerAdminDiagnosticRoutes registers infrastructure, system status, caching, database, and logs diagnostics.
func registerAdminDiagnosticRoutes(adminRouter *gin.RouterGroup) {
// System status
adminRouter.GET("/status", admin_status.GetSystemStatus)
// Database basic info & backup export
adminRouter.GET("/db-info", admin_status.GetDatabaseInfo)
adminRouter.GET("/db-export", admin_status.ExportDatabase)
// Database management & interactive browser
dbManage := adminRouter.Group("/db-manage")
{
dbManage.GET("/overview", admin_db_manage.GetDBOverview)
dbManage.GET("/tables", admin_db_manage.ListDBTables)
dbManage.GET("/table-data", admin_db_manage.GetDBTableData)
dbManage.POST("/query", admin_db_manage.ExecuteSQL)
}
// Cache management (TTL, LRU eviction and clear operations)
cache := adminRouter.Group("/cache")
{
cache.GET("/status", admin_cache.GetCacheStatus)
cache.POST("/config", admin_cache.UpdateCacheConfig)
cache.POST("/clear", admin_cache.ClearCache)
}
// Application updater
update := adminRouter.Group("/update")
{
update.GET("", admin_updater.GetUpdateStatus)
update.POST("/apply", admin_updater.ApplyUpdate)
}
// System & access logs analytics
logs := adminRouter.Group("/logs")
{
logs.GET("", admin_logs.GetLogs)
logs.GET("/access", admin_logs.GetAccessLogs)
logs.GET("/analytics", admin_logs.GetLogsAnalytics)
logs.GET("/ws", admin_logs.HandleLogWebSocket)
}
}
// registerAdminIAMRoutes registers Identity & Access Management endpoints (Users & Auth Sources).
func registerAdminIAMRoutes(adminRouter *gin.RouterGroup) {
// Users management
users := adminRouter.Group("/users")
{
users.GET("", admin_user.ListUsers)
users.POST("", admin_user.CreateUser)
users.GET("/:id", admin_user.GetUser)
users.PUT("/:id/status", admin_user.UpdateUserStatus)
users.DELETE("/:id", admin_user.DeleteUser)
}
// Authentication Sources (LDAP, OAuth sources, etc.)
authSources := adminRouter.Group("/auth-sources")
{
authSources.GET("", admin_auth_source.ListAuthSources)
authSources.POST("", admin_auth_source.CreateAuthSource)
authSources.PUT("/:id", admin_auth_source.UpdateAuthSource)
authSources.PUT("/:id/toggle", admin_auth_source.ToggleAuthSource)
authSources.DELETE("/:id", admin_auth_source.DeleteAuthSource)
}
}
// registerAdminConfigRoutes registers system configurations and template settings.
func registerAdminConfigRoutes(adminRouter *gin.RouterGroup) {
// System configs
configs := adminRouter.Group("/system-configs")
{
configs.GET("", system_config.ListSystemConfigs)
configs.POST("", system_config.CreateSystemConfig)
configs.POST("/smtp/test", system_config.TestSMTP)
keyGroup := configs.Group("/:key")
{
keyGroup.GET("", system_config.GetSystemConfig)
keyGroup.PUT("", system_config.UpdateSystemConfig)
}
}
// Email/Notification Templates
templates := adminRouter.Group("/templates")
{
templates.GET("", admin_template.ListTemplates)
templates.POST("", admin_template.CreateTemplate)
keyGroup := templates.Group("/:key")
{
keyGroup.GET("", admin_template.GetTemplate)
keyGroup.PUT("", admin_template.UpdateTemplate)
keyGroup.DELETE("", admin_template.DeleteTemplate)
}
}
}
// registerAdminStorageRoutes registers file and asset storage administration.
func registerAdminStorageRoutes(adminRouter *gin.RouterGroup) {
uploads := adminRouter.Group("/uploads")
{
uploads.GET("", upload.ListFiles)
uploads.GET("/stats", upload.GetFileStats)
uploads.DELETE("/:id", upload.DeleteFile)
uploads.GET("/download/:id", upload.DownloadFile)
uploads.POST("/download/batch", upload.BatchDownloadFiles)
uploads.GET("/types", upload.GetDistinctUploadTypes)
}
}
// registerAdminTaskRoutes registers task orchestrations, execution logs and schedules.
func registerAdminTaskRoutes(adminRouter *gin.RouterGroup) {
tasks := adminRouter.Group("/tasks")
{
// Task dispatch & metadata
tasks.GET("/types", admin_task.ListTaskTypes)
tasks.POST("/dispatch", admin_task.DispatchTask)
// Task execution logs & manual retry
executions := tasks.Group("/executions")
{
executions.GET("", admin_task.ListTaskExecutions)
executions.GET("/:id", admin_task.GetTaskExecution)
executions.POST("/:id/retry", admin_task.RetryTask)
}
// Cron scheduler settings
schedules := tasks.Group("/schedules")
{
schedules.GET("", admin_task.ListSchedules)
schedules.POST("", admin_task.CreateSchedule)
schedules.PUT("/:id", admin_task.UpdateSchedule)
schedules.DELETE("/:id", admin_task.DeleteSchedule)
}
}
}
// registerAdminPushRoutes registers messaging channels and push notification events.
func registerAdminPushRoutes(adminRouter *gin.RouterGroup) {
push := adminRouter.Group("/push")
{
// Push Events
events := push.Group("/events")
{
events.GET("", admin_push.ListEvents)
events.GET("/builtin", admin_push.ListBuiltInEvents)
events.POST("", admin_push.CreateEvent)
events.PUT("/:id", admin_push.UpdateEvent)
events.DELETE("/:id", admin_push.DeleteEvent)
events.POST("/:id/toggle", admin_push.ToggleEvent)
}
// Delivery histories and diagnostics test
push.GET("/histories", admin_push.ListHistories)
push.POST("/test", admin_push.TestPush)
// Message Channels CRUD
channels := push.Group("/channels")
{
channels.GET("/definitions", admin_push.ListChannelDefinitions)
channels.GET("", admin_push.ListChannels)
channels.POST("", admin_push.CreateChannel)
channels.PUT("/:id", admin_push.UpdateChannel)
channels.DELETE("/:id", admin_push.DeleteChannel)
channels.POST("/test", admin_push.TestChannel)
}
}
}
+18
View File
@@ -0,0 +1,18 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package v1 contains router registrations for API V1
package v1
import (
"github.com/Rain-kl/Wavelet/internal/apps/custom"
"github.com/gin-gonic/gin"
)
// RegisterCustomRoutes registers custom business routes to keep routing clean and stable.
func RegisterCustomRoutes(apiV1Router *gin.RouterGroup) {
customRouter := apiV1Router.Group("/custom")
{
customRouter.GET("/hello", custom.Hello)
}
}
+16
View File
@@ -0,0 +1,16 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package openflare registers OpenFlare HTTP routes.
// Management console APIs are mounted via RegisterV1Routes under /api/v1/d.
// Agent/Relay/Tunnel protocol routes are mounted via RegisterRoutes under /api/v1.
package openflare
import "github.com/gin-gonic/gin"
// RegisterRoutes mounts Agent/Relay/Tunnel protocol routes under the /api/v1 group.
func RegisterRoutes(apiV1Router *gin.RouterGroup) {
registerAgentRoutes(apiV1Router)
registerRelayRoutes(apiV1Router)
registerTunnelRoutes(apiV1Router)
}
@@ -0,0 +1,31 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package openflare
import (
"github.com/Rain-kl/Wavelet/internal/apps/openflare/agent"
"github.com/gin-gonic/gin"
)
func registerAgentRoutes(apiV1Router *gin.RouterGroup) {
agentRoute := apiV1Router.Group("/agent")
{
discoveryRoute := agentRoute.Group("/")
discoveryRoute.Use(agent.AgentRegisterAuth())
{
discoveryRoute.POST("/nodes/register", agent.RegisterHandler)
}
authorizedRoute := agentRoute.Group("/")
authorizedRoute.Use(agent.AgentAuth())
{
authorizedRoute.GET("/ws", agent.AgentWebSocketHandler)
authorizedRoute.POST("/nodes/heartbeat", agent.HeartbeatHandler)
authorizedRoute.GET("/config-versions/active", agent.GetActiveConfigHandler)
authorizedRoute.GET("/pages/deployments/:deployment_id/package", agent.DownloadPagesPackageHandler)
authorizedRoute.POST("/waf/ip-groups/sync", agent.SyncWAFIPGroupsHandler)
authorizedRoute.POST("/apply-logs", agent.ReportApplyLogHandler)
}
}
}
@@ -0,0 +1,19 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package openflare
import (
"github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/apply_log"
"github.com/gin-gonic/gin"
)
func registerApplyLogRoutes(apiGroup *gin.RouterGroup) {
applyLogRoute := apiGroup.Group("/apply-logs")
applyLogRoute.Use(apiutil.AdminMiddlewares()...)
{
apiutil.RegisterCollection(applyLogRoute, "GET", apply_log.GetApplyLogs)
applyLogRoute.POST("/cleanup", apply_log.CleanupApplyLogs)
}
}
@@ -0,0 +1,25 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package openflare
import (
"github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/config_version"
"github.com/gin-gonic/gin"
)
func registerConfigVersionRoutes(apiGroup *gin.RouterGroup) {
configVersionGroup := apiGroup.Group("/config-versions")
configVersionGroup.Use(apiutil.AdminMiddlewares()...)
{
apiutil.RegisterCollection(configVersionGroup, "GET", config_version.ListConfigVersionsHandler)
configVersionGroup.GET("/active", config_version.GetActiveConfigVersionHandler)
configVersionGroup.GET("/preview", config_version.PreviewConfigVersionHandler)
configVersionGroup.GET("/diff", config_version.DiffConfigVersionHandler)
configVersionGroup.GET("/:id", config_version.GetConfigVersionHandler)
configVersionGroup.POST("/publish", config_version.PublishConfigVersionHandler)
configVersionGroup.POST("/:id/activate", config_version.ActivateConfigVersionHandler)
configVersionGroup.POST("/cleanup", config_version.CleanupConfigVersionsHandler)
}
}
@@ -0,0 +1,18 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package openflare
import (
"github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/dashboard"
"github.com/gin-gonic/gin"
)
func registerDashboardRoutes(apiGroup *gin.RouterGroup) {
dashboardRoute := apiGroup.Group("/dashboard")
dashboardRoute.Use(apiutil.AdminMiddlewares()...)
{
dashboardRoute.GET("/overview", dashboard.GetOverviewHandler)
}
}
@@ -0,0 +1,29 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package openflare
import (
"github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/node"
"github.com/gin-gonic/gin"
)
func registerNodeRoutes(apiGroup *gin.RouterGroup) {
nodeRoute := apiGroup.Group("/nodes")
nodeRoute.Use(apiutil.AdminMiddlewares()...)
{
nodeRoute.GET("/bootstrap-token", node.GetBootstrapTokenHandler)
nodeRoute.POST("/bootstrap-token/rotate", node.RotateBootstrapTokenHandler)
apiutil.RegisterCollection(nodeRoute, "GET", node.ListNodesHandler)
apiutil.RegisterCollection(nodeRoute, "POST", node.CreateNodeHandler)
nodeRoute.GET("/:id/agent-release", node.GetAgentReleaseHandler)
nodeRoute.POST("/:id/update", node.UpdateNodeHandler)
nodeRoute.POST("/:id/delete", node.DeleteNodeHandler)
nodeRoute.POST("/:id/agent-update", node.RequestAgentUpdateHandler)
nodeRoute.POST("/:id/openresty-restart", node.RequestOpenrestyRestartHandler)
nodeRoute.POST("/:id/force-sync", node.RequestForceSyncHandler)
nodeRoute.GET("/:id/observability", node.GetObservabilityHandler)
nodeRoute.POST("/:id/observability/cleanup", node.CleanupHealthEventsHandler)
}
}
@@ -0,0 +1,23 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package openflare
import (
"github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/observability"
"github.com/gin-gonic/gin"
)
func registerObservabilityRoutes(apiGroup *gin.RouterGroup) {
accessLogRoute := apiGroup.Group("/access-logs")
accessLogRoute.Use(apiutil.AdminMiddlewares()...)
{
apiutil.RegisterCollection(accessLogRoute, "GET", observability.GetAccessLogsHandler)
accessLogRoute.GET("/folds", observability.GetFoldedAccessLogsHandler)
accessLogRoute.GET("/folds/ip-summary", observability.GetFoldedAccessLogIPsHandler)
accessLogRoute.GET("/ip-summary", observability.GetAccessLogIPSummariesHandler)
accessLogRoute.GET("/ip-summary/trend", observability.GetAccessLogIPTrendHandler)
accessLogRoute.POST("/cleanup", observability.CleanupAccessLogsHandler)
}
}
@@ -0,0 +1,31 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package openflare
import (
"github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/option"
"github.com/gin-gonic/gin"
)
func registerOptionRoutes(apiGroup *gin.RouterGroup) {
apiGroup.GET("/status", option.GetStatusHandler)
apiGroup.GET("/notice", option.GetNoticeHandler)
optionRoute := apiGroup.Group("/option")
optionRoute.Use(apiutil.AdminMiddlewares()...)
{
apiutil.RegisterCollection(optionRoute, "GET", option.ListOptionsHandler)
optionRoute.POST("/update", option.UpdateOptionHandler)
optionRoute.POST("/update-batch", option.UpdateOptionsBatchHandler)
optionRoute.POST("/geoip/lookup", option.LookupGeoIPHandler)
optionRoute.POST("/database/cleanup", option.CleanupDatabaseHandler)
}
uptimeKumaRoute := apiGroup.Group("/uptimekuma")
uptimeKumaRoute.Use(apiutil.AdminMiddlewares()...)
{
uptimeKumaRoute.POST("/sync", option.SyncUptimeKumaHandler)
}
}
@@ -0,0 +1,22 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package openflare
import (
"github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/origin"
"github.com/gin-gonic/gin"
)
func registerOriginRoutes(apiGroup *gin.RouterGroup) {
originRoute := apiGroup.Group("/origins")
originRoute.Use(apiutil.AdminMiddlewares()...)
{
apiutil.RegisterCollection(originRoute, "GET", origin.GetOrigins)
originRoute.GET("/:id", origin.GetOrigin)
apiutil.RegisterCollection(originRoute, "POST", origin.CreateOriginHandler)
originRoute.POST("/:id/update", origin.UpdateOriginHandler)
originRoute.POST("/:id/delete", origin.DeleteOriginHandler)
}
}
@@ -0,0 +1,27 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package openflare
import (
"github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/pages"
"github.com/gin-gonic/gin"
)
func registerPagesRoutes(apiGroup *gin.RouterGroup) {
pagesRoute := apiGroup.Group("/pages")
pagesRoute.Use(apiutil.AdminMiddlewares()...)
{
apiutil.RegisterCollection(pagesRoute, "GET", pages.ListProjectsHandler)
pagesRoute.GET("/:id", pages.GetProjectHandler)
apiutil.RegisterCollection(pagesRoute, "POST", pages.CreateProjectHandler)
pagesRoute.POST("/:id/update", pages.UpdateProjectHandler)
pagesRoute.POST("/:id/delete", pages.DeleteProjectHandler)
pagesRoute.GET("/:id/deployments", pages.ListDeploymentsHandler)
pagesRoute.POST("/:id/deployments/upload", pages.UploadDeploymentHandler)
pagesRoute.POST("/:id/deployments/:deployment_id/activate", pages.ActivateDeploymentHandler)
pagesRoute.POST("/:id/deployments/:deployment_id/delete", pages.DeleteDeploymentHandler)
pagesRoute.GET("/deployments/:deployment_id/files", pages.ListDeploymentFilesHandler)
}
}
@@ -0,0 +1,22 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package openflare
import (
"github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/proxy_route"
"github.com/gin-gonic/gin"
)
func registerProxyRouteRoutes(apiGroup *gin.RouterGroup) {
proxyRouteGroup := apiGroup.Group("/proxy-routes")
proxyRouteGroup.Use(apiutil.AdminMiddlewares()...)
{
apiutil.RegisterCollection(proxyRouteGroup, "GET", proxy_route.GetProxyRoutes)
proxyRouteGroup.GET("/:id", proxy_route.GetProxyRouteHandler)
apiutil.RegisterCollection(proxyRouteGroup, "POST", proxy_route.CreateProxyRouteHandler)
proxyRouteGroup.POST("/:id/update", proxy_route.UpdateProxyRouteHandler)
proxyRouteGroup.POST("/:id/delete", proxy_route.DeleteProxyRouteHandler)
}
}
@@ -0,0 +1,30 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package openflare
import (
"github.com/Rain-kl/Wavelet/internal/apps/openflare/flared"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/relay"
"github.com/gin-gonic/gin"
)
func registerRelayRoutes(apiV1Router *gin.RouterGroup) {
relayRoute := apiV1Router.Group("/relay")
relayRoute.Use(relay.RelayAuth())
{
relayRoute.POST("/heartbeat", relay.PostHeartbeat)
relayRoute.GET("/ws", relay.GetWebSocket)
}
}
func registerTunnelRoutes(apiV1Router *gin.RouterGroup) {
tunnelRoute := apiV1Router.Group("/tunnel")
tunnelRoute.Use(flared.TunnelAuth())
{
tunnelRoute.POST("/heartbeat", flared.PostHeartbeat)
tunnelRoute.GET("/config/active", flared.GetActiveConfig)
tunnelRoute.POST("/apply-log", flared.PostApplyLog)
tunnelRoute.GET("/ws", flared.GetWebSocket)
}
}
@@ -0,0 +1,53 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package openflare
import (
"github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/tls"
"github.com/gin-gonic/gin"
)
func registerTLSRoutes(apiGroup *gin.RouterGroup) {
managedDomainRoute := apiGroup.Group("/managed-domains")
managedDomainRoute.Use(apiutil.AdminMiddlewares()...)
{
apiutil.RegisterCollection(managedDomainRoute, "GET", tls.GetManagedDomains)
managedDomainRoute.GET("/match", tls.MatchManagedDomainCertificateHandler)
apiutil.RegisterCollection(managedDomainRoute, "POST", tls.CreateManagedDomainHandler)
managedDomainRoute.POST("/:id/update", tls.UpdateManagedDomainHandler)
managedDomainRoute.POST("/:id/delete", tls.DeleteManagedDomainHandler)
}
tlsCertificateRoute := apiGroup.Group("/tls-certificates")
tlsCertificateRoute.Use(apiutil.AdminMiddlewares()...)
{
apiutil.RegisterCollection(tlsCertificateRoute, "GET", tls.GetCertificates)
tlsCertificateRoute.GET("/:id", tls.GetCertificateDetail)
tlsCertificateRoute.GET("/:id/content", tls.GetCertificateContentHandler)
apiutil.RegisterCollection(tlsCertificateRoute, "POST", tls.CreateCertificateHandler)
tlsCertificateRoute.POST("/:id/update", tls.UpdateCertificateHandler)
tlsCertificateRoute.POST("/:id/update-acme", tls.UpdateACMECertificateHandler)
tlsCertificateRoute.POST("/:id/convert-acme", tls.ConvertCertificateToACMEHandler)
tlsCertificateRoute.POST("/import-file", tls.ImportCertificateFile)
tlsCertificateRoute.POST("/:id/delete", tls.DeleteCertificateHandler)
tlsCertificateRoute.POST("/apply", tls.ApplyCertificateHandler)
tlsCertificateRoute.POST("/:id/renew", tls.RenewCertificateHandler)
}
acmeAccountRoute := apiGroup.Group("/acme-accounts")
acmeAccountRoute.Use(apiutil.AdminMiddlewares()...)
{
acmeAccountRoute.GET("/default", tls.GetDefaultAcmeAccountHandler)
}
dnsAccountRoute := apiGroup.Group("/dns-accounts")
dnsAccountRoute.Use(apiutil.AdminMiddlewares()...)
{
apiutil.RegisterCollection(dnsAccountRoute, "GET", tls.GetDNSAccounts)
apiutil.RegisterCollection(dnsAccountRoute, "POST", tls.CreateDNSAccountHandler)
dnsAccountRoute.POST("/:id/update", tls.UpdateDNSAccountHandler)
dnsAccountRoute.POST("/:id/delete", tls.DeleteDNSAccountHandler)
}
}
@@ -0,0 +1,34 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package openflare
import (
"github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/waf"
"github.com/gin-gonic/gin"
)
func registerWAFRoutes(apiGroup *gin.RouterGroup) {
wafRoute := apiGroup.Group("/waf")
wafRoute.Use(apiutil.AdminMiddlewares()...)
{
wafRoute.GET("/ip-groups", waf.ListIPGroupsHandler)
wafRoute.GET("/ip-groups/:id", waf.GetIPGroupHandler)
wafRoute.POST("/ip-groups", waf.CreateIPGroupHandler)
wafRoute.POST("/ip-groups/test", waf.TestIPGroupAutoConfigHandler)
wafRoute.POST("/ip-groups/:id/update", waf.UpdateIPGroupHandler)
wafRoute.POST("/ip-groups/:id/delete", waf.DeleteIPGroupHandler)
wafRoute.POST("/ip-groups/:id/sync", waf.SyncIPGroupHandler)
wafRoute.GET("/rule-groups", waf.ListRuleGroupsHandler)
wafRoute.GET("/rule-groups/:id", waf.GetRuleGroupHandler)
wafRoute.POST("/rule-groups", waf.CreateRuleGroupHandler)
wafRoute.POST("/rule-groups/:id/update", waf.UpdateRuleGroupHandler)
wafRoute.POST("/rule-groups/:id/delete", waf.DeleteRuleGroupHandler)
wafRoute.POST("/rule-groups/:id/sites", waf.ReplaceRuleGroupSitesHandler)
wafRoute.GET("/sites/:route_id/rule-groups", waf.GetSiteRuleGroupsHandler)
wafRoute.POST("/sites/:route_id/rule-groups", waf.ReplaceSiteRuleGroupsHandler)
}
}
+25
View File
@@ -0,0 +1,25 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package openflare
import "github.com/gin-gonic/gin"
// V1BasePath is the OpenFlare console API prefix under /api/v1.
const V1BasePath = "/api/v1/d"
// RegisterV1Routes mounts OpenFlare management console APIs under /api/v1/d.
func RegisterV1Routes(apiV1Router *gin.RouterGroup) {
group := apiV1Router.Group("/d")
registerOptionRoutes(group)
registerOriginRoutes(group)
registerApplyLogRoutes(group)
registerProxyRouteRoutes(group)
registerNodeRoutes(group)
registerWAFRoutes(group)
registerTLSRoutes(group)
registerConfigVersionRoutes(group)
registerPagesRoutes(group)
registerDashboardRoutes(group)
registerObservabilityRoutes(group)
}
+95
View File
@@ -0,0 +1,95 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package v1 contains router registrations for API V1
package v1
import (
capApp "github.com/Rain-kl/Wavelet/internal/apps/cap"
publicconfig "github.com/Rain-kl/Wavelet/internal/apps/config"
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
"github.com/Rain-kl/Wavelet/internal/apps/upload"
"github.com/Rain-kl/Wavelet/internal/apps/user"
"github.com/gin-gonic/gin"
)
// RegisterUserRoutes registers all user-related, oauth, upload, and public routes.
func RegisterUserRoutes(apiV1Router *gin.RouterGroup, apiGroup *gin.RouterGroup) {
// 1. CAPTCHA
registerCaptchaRoutes(apiGroup)
// 2. Config (public)
registerConfigRoutes(apiV1Router)
// 3. OAuth
registerOAuthRoutes(apiV1Router)
// 4. User
registerUserRoutes(apiV1Router)
// 5. Upload
registerUploadRoutes(apiV1Router)
}
func registerCaptchaRoutes(apiGroup *gin.RouterGroup) {
capGroup := apiGroup.Group("/cap")
{
capGroup.POST("/challenge", capApp.Challenge)
capGroup.POST("/redeem", capApp.Redeem)
}
}
func registerConfigRoutes(apiV1Router *gin.RouterGroup) {
configRouter := apiV1Router.Group("/config")
{
configRouter.GET("/public", publicconfig.GetPublicConfig)
}
}
func registerOAuthRoutes(apiV1Router *gin.RouterGroup) {
apiV1Router.GET("/oauth/sources", oauth.GetLoginSources)
apiV1Router.GET("/oauth/login", oauth.GetLoginURL)
apiV1Router.GET("/oauth/:source/authorize", oauth.Authorize)
apiV1Router.GET("/oauth/logout", oauth.Logout)
apiV1Router.POST("/oauth/callback", oauth.Callback)
apiV1Router.GET("/oauth/user-info", oauth.LoginRequired(), oauth.UserInfo)
apiV1Router.GET("/user-info", oauth.LoginRequired(), oauth.UserInfo)
apiV1Router.GET("/oauth/external-accounts", oauth.LoginRequired(), oauth.ListExternalAccounts)
apiV1Router.POST("/oauth/external-accounts/:id/delete", oauth.LoginRequired(), oauth.DeleteExternalAccount)
}
func registerUserRoutes(apiV1Router *gin.RouterGroup) {
userRouter := apiV1Router.Group("/user")
{
userRouter.POST("/login", capApp.VerifyMiddleware(capApp.GetDefaultManager(), "login"), user.Login)
userRouter.POST("/register", capApp.VerifyMiddleware(capApp.GetDefaultManager(), "register"), user.Register)
userRouter.POST("/send-email-code", capApp.VerifyMiddleware(capApp.GetDefaultManager(), "send_email_code"), user.SendEmailCode)
userRouter.GET("/logout", user.Logout)
userRouter.GET("/self", oauth.LoginRequired(), oauth.UserInfo)
userRouter.POST("/change-password", oauth.LoginRequired(), user.ChangePassword)
userRouter.PUT("/profile", oauth.LoginRequired(), user.UpdateProfile)
// Access Token
tokenRouter := userRouter.Group("/access-tokens")
tokenRouter.Use(oauth.LoginRequired(), oauth.DisallowTokenAuth())
{
tokenRouter.GET("", user.ListAccessTokens)
tokenRouter.POST("", user.CreateAccessToken)
tokenRouter.DELETE("/:id", user.DeleteAccessToken)
tokenRouter.POST("/:id/rotate", user.RotateAccessToken)
}
}
}
func registerUploadRoutes(apiV1Router *gin.RouterGroup) {
uploadRouter := apiV1Router.Group("/upload")
uploadRouter.Use(oauth.LoginRequired())
{
uploadRouter.POST("", upload.UploadFile)
uploadRouter.GET("/my", upload.ListMyFiles)
uploadRouter.DELETE("/:id", upload.DeleteMyFile)
uploadRouter.PUT("/:id", upload.UpdateMyFile)
uploadRouter.GET("/download/:id", upload.DownloadFile)
uploadRouter.POST("/download/batch", upload.BatchDownloadFiles)
}
}
+26
View File
@@ -0,0 +1,26 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package v1 contains router registrations for API V1
package v1
import (
ofrouter "github.com/Rain-kl/Wavelet/internal/router/v1/openflare"
"github.com/gin-gonic/gin"
)
// RegisterV1Routes registers all routes under API V1.
func RegisterV1Routes(apiV1Router *gin.RouterGroup, apiGroup *gin.RouterGroup) {
// 1. User & Public routes (OAuth, User, Upload, CAPTCHA, Health, Config)
RegisterUserRoutes(apiV1Router, apiGroup)
// 2. Admin routes
RegisterAdminRoutes(apiV1Router)
// 3. OpenFlare management console APIs and Agent/Relay/Tunnel protocol routes
ofrouter.RegisterV1Routes(apiV1Router)
ofrouter.RegisterRoutes(apiV1Router)
// 4. Custom business routes (example only)
RegisterCustomRoutes(apiV1Router)
}