diff --git a/admin/client/parser.py b/admin/client/parser.py index e82517f16b..788e345992 100644 --- a/admin/client/parser.py +++ b/admin/client/parser.py @@ -96,6 +96,7 @@ sql_command: login_user | show_fingerprint | set_license | show_license + | check_license | benchmark // meta command definition @@ -183,6 +184,7 @@ SESSIONS: "SESSIONS"i SERVER: "SERVER"i FINGERPRINT: "FINGERPRINT"i LICENSE: "LICENSE"i +CHECK: "CHECK"i login_user: LOGIN USER quoted_string ";" list_services: LIST SERVICES ";" @@ -231,6 +233,7 @@ list_environments: LIST ENVS ";" show_fingerprint: SHOW FINGERPRINT ";" set_license: SET LICENSE quoted_string ";" show_license: SHOW LICENSE ";" +check_license: CHECK LICENSE ";" list_server_configs: LIST SERVER CONFIGS ";" @@ -496,6 +499,9 @@ class RAGFlowCLITransformer(Transformer): def show_license(self, items): return {"type": "show_license"} + def check_license(self, items): + return {"type": "check_license"} + def list_server_configs(self, items): return {"type": "list_server_configs"} diff --git a/admin/client/ragflow_client.py b/admin/client/ragflow_client.py index b59b931bfe..d878e9e4aa 100644 --- a/admin/client/ragflow_client.py +++ b/admin/client/ragflow_client.py @@ -614,6 +614,17 @@ class RAGFlowClient: else: print(f"Fail to show license, code: {res_json['code']}, message: {res_json['message']}") + def check_license(self, command): + if self.server_type != "admin": + print("This command is only allowed in ADMIN mode") + response = self.http_client.request("GET", "/admin/license?check=true", use_api_base=True, auth_kind="admin") + res_json = response.json() + if response.status_code == 200: + print(res_json["data"]) + else: + print(f"Fail to show license, code: {res_json['code']}, message: {res_json['message']}") + + def list_server_configs(self, command): """List server configs by calling /system/configs API and flattening the JSON response.""" response = self.http_client.request("GET", "/system/configs", use_api_base=False, auth_kind="web") @@ -1551,6 +1562,8 @@ def run_command(client: RAGFlowClient, command_dict: dict): client.set_license(command_dict) case "show_license": client.show_license(command_dict) + case "check_license": + client.check_license(command_dict) case "list_server_configs": client.list_server_configs(command_dict) case "create_model_provider": diff --git a/build.sh b/build.sh index 70fe162437..5c075120d1 100755 --- a/build.sh +++ b/build.sh @@ -92,7 +92,8 @@ build_go() { echo "Building Go binary: $OUTPUT_BINARY" GOPROXY=${GOPROXY:-https://goproxy.cn,https://proxy.golang.org,direct} CGO_ENABLED=1 go build -o "$OUTPUT_BINARY" ./cmd/server_main.go - + GOPROXY=${GOPROXY:-https://goproxy.cn,https://proxy.golang.org,direct} CGO_ENABLED=1 go build -o "$OUTPUT_BINARY" ./cmd/admin_server.go + if [ ! -f "$OUTPUT_BINARY" ]; then echo -e "${RED}Error: Failed to build Go binary${NC}" exit 1 diff --git a/cmd/server_main.go b/cmd/server_main.go index 81e66080e2..a6eb3408bb 100644 --- a/cmd/server_main.go +++ b/cmd/server_main.go @@ -9,6 +9,7 @@ import ( "os/signal" "ragflow/internal/common" "ragflow/internal/server" + "ragflow/internal/server/local" "ragflow/internal/utility" "strings" "syscall" @@ -123,6 +124,9 @@ func main() { logger.Warn("Failed to initialize server variables from Redis, using defaults", zap.String("error", err.Error())) } + // Initialize admin status (default: unavailable=1) + local.InitAdminStatus(1, "admin server not connected") + // Initialize tokenizer (rag_analyzer) tokenizerCfg := &tokenizer.PoolConfig{ DictPath: "/usr/share/infinity/resource", @@ -238,7 +242,11 @@ func startServer(config *server.Config) { } else { // Start heartbeat reporter with 30 seconds interval heartbeatReporter := utility.NewScheduledTask("Heartbeat reporter", 3*time.Second, func() { - if err := heartbeatService.SendHeartbeat(); err != nil { + var message string + if err, message = heartbeatService.SendHeartbeat(); err == nil { + local.SetAdminStatus(0, "") + } else { + local.SetAdminStatus(1, message) logger.Warn("Failed to send heartbeat", zap.Error(err)) } }) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index f6557b3e4e..9497d8c6a1 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -249,6 +249,7 @@ if [[ "${ENABLE_ADMIN_SERVER}" -eq 1 ]]; then echo "Starting admin_server..." while true; do "$PY" admin/server/admin_server.py & + bin/admin_server & wait; sleep 1; done & diff --git a/internal/handler/auth.go b/internal/handler/auth.go index ca232645a0..57e7a29ccd 100644 --- a/internal/handler/auth.go +++ b/internal/handler/auth.go @@ -17,8 +17,11 @@ package handler import ( + "fmt" "net/http" "ragflow/internal/common" + "ragflow/internal/logger" + "ragflow/internal/server/local" "ragflow/internal/service" "github.com/gin-gonic/gin" @@ -69,13 +72,21 @@ func (h *AuthHandler) AuthMiddleware() gin.HandlerFunc { return } + if !local.IsAdminAvailable() { + license := local.GetAdminStatus() + errMsg := fmt.Sprintf("server license %s, check admin server status", license.Reason) + logger.Warn(errMsg) + c.JSON(http.StatusServiceUnavailable, gin.H{ + "code": common.CodeUnauthorized, + "message": errMsg, + "data": "No", + }) + return + } + c.Set("user", user) c.Set("user_id", user.ID) c.Set("email", user.Email) c.Next() } } - -func (h *AuthHandler) LoginByEmail1(c *gin.Context) { - println("hello") -} diff --git a/internal/handler/user.go b/internal/handler/user.go index 2678ecf1bf..96f3449804 100644 --- a/internal/handler/user.go +++ b/internal/handler/user.go @@ -21,6 +21,7 @@ import ( "net/http" "ragflow/internal/common" "ragflow/internal/server" + "ragflow/internal/server/local" "ragflow/internal/utility" "strconv" @@ -164,6 +165,16 @@ func (h *UserHandler) LoginByEmail(c *gin.Context) { return } + if !local.IsAdminAvailable() { + license := local.GetAdminStatus() + c.JSON(http.StatusOK, gin.H{ + "code": common.CodeAuthenticationError, + "message": license.Reason, + "data": "No", + }) + return + } + user, code, err := h.userService.LoginByEmail(&req, false) if err != nil { c.JSON(http.StatusOK, gin.H{ @@ -291,14 +302,38 @@ func (h *UserHandler) ListUsers(c *gin.Context) { // @Success 200 {object} map[string]interface{} // @Router /v1/user/logout [post] func (h *UserHandler) Logout(c *gin.Context) { - user, errorCode, errorMessage := GetUser(c) - if errorCode != common.CodeSuccess { - jsonError(c, errorCode, errorMessage) + // Same as AuthMiddleware@auth.go + token := c.GetHeader("Authorization") + if token == "" { + c.JSON(http.StatusUnauthorized, gin.H{ + "code": 401, + "message": "Missing Authorization header", + }) + c.Abort() + return + } + + // Get user by access token + user, code, err := h.userService.GetUserByToken(token) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{ + "code": code, + "message": "Invalid access token", + }) + c.Abort() + return + } + + if *user.IsSuperuser { + c.JSON(http.StatusForbidden, gin.H{ + "code": common.CodeForbidden, + "message": "Super user should access the URL", + }) return } // Logout user - code, err := h.userService.Logout(user) + code, err = h.userService.Logout(user) if err != nil { c.JSON(http.StatusOK, gin.H{ "code": code, diff --git a/internal/router/router.go b/internal/router/router.go index b7f8b0a671..1085e0c4c9 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -93,12 +93,13 @@ func (r *Router) Setup(engine *gin.Engine) { // User login by email endpoint engine.POST("/v1/user/login", r.userHandler.LoginByEmail) + // User logout endpoint + engine.GET("/v1/user/logout", r.userHandler.Logout) + // Protected routes authorized := engine.Group("") authorized.Use(r.authHandler.AuthMiddleware()) { - // User logout endpoint - authorized.GET("/v1/user/logout", r.userHandler.Logout) // User info endpoint authorized.GET("/v1/user/info", r.userHandler.Info) // User tenant info endpoint @@ -116,13 +117,13 @@ func (r *Router) Setup(engine *gin.Engine) { v1 := authorized.Group("/api/v1") { // User routes - users := v1.Group("/users") - { - users.POST("/register", r.userHandler.Register) - users.POST("/login", r.userHandler.Login) - users.GET("", r.userHandler.ListUsers) - users.GET("/:id", r.userHandler.GetUserByID) - } + //users := v1.Group("/users") + //{ + // users.POST("/register", r.userHandler.Register) + // users.POST("/login", r.userHandler.Login) + // users.GET("", r.userHandler.ListUsers) + // users.GET("/:id", r.userHandler.GetUserByID) + //} // Document routes documents := v1.Group("/documents") diff --git a/internal/server/local/admin_status.go b/internal/server/local/admin_status.go new file mode 100644 index 0000000000..5c2e8ab298 --- /dev/null +++ b/internal/server/local/admin_status.go @@ -0,0 +1,79 @@ +// +// Copyright 2026 The InfiniFlow Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package local + +import ( + "sync" +) + +// AdminStatus represents the admin status +// 0 = valid, 1 = invalid +type AdminStatus struct { + Status int `json:"status"` // 0 = available, 1 = not available + Reason string `json:"reason"` // reason for invalid status +} + +var ( + adminStatus *AdminStatus + adminStatusMu sync.RWMutex + adminStatusOnce sync.Once +) + +// InitAdminStatus initializes the global admin status +// status: 0 = valid, 1 = invalid (default) +func InitAdminStatus(status int, reason string) { + adminStatusOnce.Do(func() { + adminStatus = &AdminStatus{ + Status: status, + Reason: reason, + } + }) +} + +// GetAdminStatus returns the current admin status +func GetAdminStatus() AdminStatus { + adminStatusMu.RLock() + defer adminStatusMu.RUnlock() + if adminStatus == nil { + return AdminStatus{Status: 1, Reason: "not initialized"} + } + return AdminStatus{ + Status: adminStatus.Status, + Reason: adminStatus.Reason, + } +} + +// SetAdminStatus updates the admin status +func SetAdminStatus(status int, reason string) { + adminStatusMu.Lock() + defer adminStatusMu.Unlock() + if adminStatus == nil { + adminStatus = &AdminStatus{} + } + adminStatus.Status = status + adminStatus.Reason = reason +} + +// IsAdminAvailable returns true if admin is valid (Status == 0) +func IsAdminAvailable() bool { + adminStatusMu.RLock() + defer adminStatusMu.RUnlock() + if adminStatus == nil { + return false + } + return adminStatus.Status == 0 +} diff --git a/internal/service/heartbeat_sender.go b/internal/service/heartbeat_sender.go index 3d7539848b..47c4d67550 100644 --- a/internal/service/heartbeat_sender.go +++ b/internal/service/heartbeat_sender.go @@ -76,12 +76,12 @@ func (h *HeartbeatSender) InitHTTPClient() error { } // SendHeartbeat sends a heartbeat message to the admin server -func (h *HeartbeatSender) SendHeartbeat() error { +func (h *HeartbeatSender) SendHeartbeat() (error, string) { if h.attemptCount < 10 { if h.lastSuccess { h.attemptCount++ - return nil + return nil, "" } } h.attemptCount = 0 @@ -90,7 +90,7 @@ func (h *HeartbeatSender) SendHeartbeat() error { if h.client == nil { if err := h.InitHTTPClient(); err != nil { h.logger.Error("Failed to initialize HTTP client", zap.Error(err)) - return err + return err, "internal error, fail to initialize HTTP client" } } @@ -109,19 +109,19 @@ func (h *HeartbeatSender) SendHeartbeat() error { jsonData, err := json.Marshal(message) if err != nil { h.logger.Error("Failed to marshal heartbeat message", zap.Error(err)) - return err + return err, "fail to parse the message" } resp, err := h.client.PostJSON("/api/v1/admin/reports", jsonData) if err != nil { - return err + return err, "can't connect with admin server" } defer resp.Body.Close() if resp.StatusCode != 200 { errMsg := fmt.Errorf("Heartbeat request failed with status code: %d", resp.StatusCode) h.logger.Warn(errMsg.Error()) - return errMsg + return errMsg, errMsg.Error() } h.logger.Debug("Heartbeat sent successfully", @@ -131,5 +131,5 @@ func (h *HeartbeatSender) SendHeartbeat() error { h.lastSuccess = true - return nil + return nil, "" }