mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-31 20:32:23 +03:00
* test(web): pin the endpoints.ts registry to the actual Gin routes endpoints.ts is a hand-maintained registry and nothing checked it against the router: an omitted API route silently vanishes from the generated OpenAPI docs, and an entry for a removed route documents an endpoint that 404s. Two new tests construct the real router against a throwaway DB and diff the /panel/api surface both ways. The check found one gap on arrival: GET /panel/api/openapi.json — the endpoint that serves the docs — was itself undocumented. Registered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api)+test: authenticate openapi.json, fold the two route-contract tests into one Three things from the review, in severity order. The bot found that GET /panel/api/openapi.json was registered on the base-path group one line before the /panel/api group installs checkAPIAuth, so Gin's snapshot of the parent chain meant the whole admin API surface plus build version was fetchable without a session — while this very PR was about to document it as auth-required. Move the registration inside the authed api group. Verified: unauthenticated it now 404s exactly like server/status (was 200), and a logged-in session still serves it 200, so the docs page is unaffected. The existing api_docs_test.go already checked the forward direction by regex-scanning controller source against a hand-maintained per-file path switch — which is why it missed this web.go-registered route, and whose fall-through default silently mis-paths any unlisted controller file. The new router-based test is a strict superset, so fold in the extra surface it guarded (/login, /logout, /csrf-token, /getTwoFactorEnable, /ws) and delete the old test rather than run two. Harden the endpoints.ts parser: pair each method with the next path sequentially instead of a brace-crossing regex, and fail loudly when the parsed count doesn't match the declared method fields. Construct the server once across both subtests, cancel it, and restore the previous global on cleanup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
117 lines
3.5 KiB
Go
117 lines
3.5 KiB
Go
package controller
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/middleware"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/service/panel"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/service/tgbot"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/session"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// APIController handles the main API routes for the 3x-ui panel, including inbounds and server management.
|
|
type APIController struct {
|
|
BaseController
|
|
inboundController *InboundController
|
|
serverController *ServerController
|
|
nodeController *NodeController
|
|
hostController *HostController
|
|
settingController *SettingController
|
|
xraySettingController *XraySettingController
|
|
userService panel.UserService
|
|
apiTokenService panel.ApiTokenService
|
|
Tgbot tgbot.Tgbot
|
|
}
|
|
|
|
// NewAPIController creates a new APIController instance and initializes its routes.
|
|
func NewAPIController(g *gin.RouterGroup) *APIController {
|
|
a := &APIController{}
|
|
a.initRouter(g)
|
|
return a
|
|
}
|
|
|
|
func (a *APIController) checkAPIAuth(c *gin.Context) {
|
|
// A verified client certificate (a completed mTLS handshake) authenticates
|
|
// the caller, equivalent to a valid bearer token. api_authed must be set so
|
|
// the CSRF middleware lets cert-authed mutations through.
|
|
if c.Request.TLS != nil && len(c.Request.TLS.VerifiedChains) > 0 {
|
|
if u, err := a.userService.GetFirstUser(); err == nil {
|
|
session.SetAPIAuthUser(c, u)
|
|
}
|
|
c.Set("api_authed", true)
|
|
c.Next()
|
|
return
|
|
}
|
|
auth := c.GetHeader("Authorization")
|
|
if after, ok := strings.CutPrefix(auth, "Bearer "); ok {
|
|
tok := after
|
|
if a.apiTokenService.Match(tok) {
|
|
if u, err := a.userService.GetFirstUser(); err == nil {
|
|
session.SetAPIAuthUser(c, u)
|
|
}
|
|
c.Set("api_authed", true)
|
|
c.Next()
|
|
return
|
|
}
|
|
}
|
|
if !session.IsLogin(c) {
|
|
if c.GetHeader("X-Requested-With") == "XMLHttpRequest" {
|
|
c.AbortWithStatus(http.StatusUnauthorized)
|
|
} else {
|
|
c.AbortWithStatus(http.StatusNotFound)
|
|
}
|
|
return
|
|
}
|
|
c.Next()
|
|
}
|
|
|
|
// initRouter sets up the API routes for inbounds, server, and other endpoints.
|
|
func (a *APIController) initRouter(g *gin.RouterGroup) {
|
|
// Main API group
|
|
api := g.Group("/panel/api")
|
|
api.Use(a.checkAPIAuth)
|
|
// Decode + verify the node config envelope (zstd + X-Config-Sha256) and
|
|
// advertise support, before CSRF/handlers read the body.
|
|
api.Use(middleware.ConfigEnvelopeMiddleware())
|
|
api.Use(middleware.CSRFMiddleware())
|
|
|
|
api.GET("/openapi.json", ServeOpenAPISpec)
|
|
|
|
// Inbounds API
|
|
inbounds := api.Group("/inbounds")
|
|
a.inboundController = NewInboundController(inbounds)
|
|
|
|
clients := api.Group("/clients")
|
|
NewClientController(clients)
|
|
NewGroupController(clients)
|
|
|
|
// Server API
|
|
server := api.Group("/server")
|
|
a.serverController = NewServerController(server)
|
|
|
|
// Nodes API — multi-panel management
|
|
nodes := api.Group("/nodes")
|
|
a.nodeController = NewNodeController(nodes)
|
|
|
|
// Hosts API — per-inbound override endpoints for subscription links
|
|
hosts := api.Group("/hosts")
|
|
a.hostController = NewHostController(hosts)
|
|
|
|
// Settings + Xray config management live under the API surface too, so the
|
|
// same API token drives them. Paths are /panel/api/setting/* and
|
|
// /panel/api/xray/*.
|
|
a.settingController = NewSettingController(api)
|
|
a.xraySettingController = NewXraySettingController(api)
|
|
|
|
// Extra routes
|
|
api.POST("/backuptotgbot", a.BackuptoTgbot)
|
|
}
|
|
|
|
// BackuptoTgbot sends a backup of the panel data to Telegram bot admins.
|
|
func (a *APIController) BackuptoTgbot(c *gin.Context) {
|
|
a.Tgbot.SendBackupToAdmins()
|
|
}
|